@viritura/score-engine 0.1.0-next.1 → 0.1.1-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["<define:import.meta.env>", "../src/index.ts", "../src/engine.ts", "../../renderer/src/index.ts", "../../core/src/constants.ts", "../../core/src/model/event.ts", "../../core/src/model/sequenceWalk.ts", "../../core/src/model/pitch.ts", "../../core/src/model/time.ts", "../../core/src/model/staffMeter.ts", "../../core/src/id.ts", "../../core/src/operations/index.ts", "../../core/src/operations/chordSymbols/index.ts", "../../core/src/operations/chordSymbols/text.ts", "../../core/src/operations/chordSymbols/resolution.ts", "../../core/src/operations/chordSymbols/modifiers.ts", "../../core/src/diagnostics.ts", "../../renderer/src/displayListPainter.ts", "../../renderer/src/basePath.ts", "../../renderer/src/textFont.ts", "../../renderer/src/binaryDisplayList.ts", "../../renderer/src/binaryDisplayListCommands.ts", "../../renderer/src/renderCommandBounds.ts", "../../renderer/src/pageCache.ts", "../../renderer/src/wasm.ts", "../../renderer/src/tileCache.ts", "../../renderer/src/paperPagePainter.ts", "../../renderer/src/playheadPainter.ts", "../src/buildInfo.ts", "../src/displayListHandle.ts", "../src/errors.ts", "../src/geometry.ts", "../src/layoutRequest.ts", "../src/layoutWorkerClient.ts", "../src/paint.ts", "../src/svgExport.ts", "../src/tileRenderer.ts", "../src/timelineBuilder.ts", "../../format/src/index.ts", "../../format/src/mnx/index.ts", "../../format/src/mnx/parser.ts", "../../format/src/mnx/parserCore.ts", "../../format/src/mnx/parseGlobal.ts", "../../format/src/mnx/parseParts.ts", "../../format/src/mnx/parseContent.ts", "../../format/src/mnx/parseLayout.ts", "../../format/src/mnx/diagnostics.ts", "../../midi/src/index.ts", "../../midi/src/timeline.ts", "../../midi/src/repeatExpansion.ts", "../../midi/src/measureRepeats.ts", "../../midi/src/chordPlayback/index.ts", "../../midi/src/sectionDynamics/index.ts", "../../midi/src/holds.ts", "../../midi/src/staffMeterTiming.ts", "../../midi/src/cadenzaTiming.ts", "../../midi/src/legato.ts", "../../midi/src/dynamics.ts", "../../midi/src/technique.ts", "../../midi/src/tempoMap.ts", "../../midi/src/tempoModel.ts", "../../midi/src/dynamicsEnvelope.ts", "../../midi/src/dynamicPlayback/index.ts", "../../midi/src/dynamicPlayback/dynamicProgram.ts", "../../midi/src/dynamicPlayback/dynamicRealization.ts", "../src/partIds.ts"],
|
|
4
|
-
"sourcesContent": ["{}", "/**\n * @viritura/score-engine \u2014 framework-free music notation rendering kernel.\n *\n * MNX in; layout, ink-only Canvas painting, SVG, geometry queries and a\n * playback timeline out. Page presentation (paper, zoom, scrolling, view\n * modes) belongs to `@viritura/score-viewer`.\n *\n * ```ts\n * import { loadEngine } from \"@viritura/score-engine\";\n *\n * const engine = await loadEngine();\n * const dl = engine.layout(mnx, { pageWidth: 800 });\n * const ctx = canvas.getContext(\"2d\")!;\n * ctx.fillStyle = \"#fff\";\n * ctx.fillRect(0, 0, canvas.width, canvas.height);\n * engine.paint(ctx, dl, { page: 0 });\n * ```\n */\n\nexport { loadEngine, isEngineReady } from \"./engine\";\nexport type { Engine } from \"./engine\";\nexport { EngineLoadError, ParseError, LayoutError } from \"./errors\";\nexport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n DisplayList,\n EngineVersion,\n HorizonPaper,\n LayoutOptions,\n LayoutPageSetup,\n LayoutWorker,\n LayoutWorkerOptions,\n LoadEngineOptions,\n MeasureGeometry,\n PageGeometry,\n PaintOptions,\n PartInfo,\n PlayheadGeometry,\n Rect,\n ScoreInfo,\n ScoreMeasurements,\n ScorePosition,\n SvgOptions,\n SystemGeometry,\n TilePaintOptions,\n TileRenderer,\n} from \"./types\";\nexport type { DynamicMark, TempoSegment, TimedEvent, Timeline, TimelineOptions } from \"./timeline\";\n", "/**\n * `loadEngine()` and the `Engine` handle it resolves to.\n *\n * The engine is the rendering kernel: MNX in, layout + ink + geometry +\n * timeline out. It never touches the DOM beyond the canvas context it is\n * given; page presentation, zoom, scrolling and UI live in the viewer layer.\n */\n\nimport {\n getEngineVersion,\n getScoreInfo,\n getWasmInitError,\n initWasm,\n isWasmReady,\n loadMusicFont,\n resolveBasePath,\n setAssetBasePath,\n wasmComputeMnxScoreLayout,\n} from \"@viritura/renderer\";\nimport { buildCommit, bundledAssetBase, PACKAGE_VERSION } from \"./buildInfo\";\nimport { unwrapDisplayList, wrapDisplayList } from \"./displayListHandle\";\nimport { EngineLoadError, ParseError } from \"./errors\";\nimport {\n canvasToBeat,\n horizonPaper,\n measureGeometry,\n measureLayout,\n playheadAt,\n positionToCanvas,\n systemGeometry,\n} from \"./geometry\";\nimport { layoutArgs, layoutFailure, messageOf, mnxJson } from \"./layoutRequest\";\nimport { createLayoutWorker } from \"./layoutWorkerClient\";\nimport { paintInk } from \"./paint\";\nimport { exportPageSvg } from \"./svgExport\";\nimport { createTileRenderer } from \"./tileRenderer\";\nimport type { Timeline, TimelineOptions } from \"./timeline\";\nimport { buildTimeline } from \"./timelineBuilder\";\nimport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n DisplayList,\n EngineVersion,\n HorizonPaper,\n LayoutOptions,\n LayoutWorker,\n LayoutWorkerOptions,\n LoadEngineOptions,\n MeasureGeometry,\n PaintOptions,\n PlayheadGeometry,\n ScoreInfo,\n ScoreMeasurements,\n ScorePosition,\n SvgOptions,\n SystemGeometry,\n TileRenderer,\n} from \"./types\";\n\n/** The loaded score engine. Acquire via {@link loadEngine}. */\nexport interface Engine {\n readonly version: EngineVersion;\n\n /**\n * Lay out an MNX document. Throws `ParseError` for unreadable input and\n * `LayoutError` if the engine fails.\n */\n layout(mnx: string | object, opts: LayoutOptions): DisplayList;\n\n /**\n * Paint one page's ink into `ctx` under the caller's current transform.\n * Never clears the canvas or paints paper unless `opts.background` is set.\n */\n paint(ctx: CanvasRenderingContext2D, displayList: DisplayList, opts?: PaintOptions): void;\n\n /** Page sizes and parts of a layout. */\n measure(displayList: DisplayList): ScoreMeasurements;\n /** Systems in reading order, page-local. */\n systems(displayList: DisplayList): SystemGeometry[];\n /** Every rendered measure/staff, page-local. */\n measures(displayList: DisplayList): MeasureGeometry[];\n /** Paper rectangle for unpaged (horizon) layouts. */\n horizonPaper(displayList: DisplayList): HorizonPaper;\n\n /** Where a musical position lands on its part's (top) staff. */\n positionToCanvas(displayList: DisplayList, position: ScorePosition): CanvasBeatPosition | null;\n /** A cursor spanning the whole system at a musical position. */\n playhead(displayList: DisplayList, position: ScorePosition): PlayheadGeometry | null;\n /** Hit-test a page-local point to a musical position. */\n canvasToBeat(displayList: DisplayList, page: number, x: number, y: number): CanvasBeatHit | null;\n\n /** Parts, measure count and score entries, without running layout. */\n info(mnx: string | object): ScoreInfo;\n /** Layout-independent playback timeline. Deterministic. */\n timeline(mnx: string | object, opts?: TimelineOptions): Timeline;\n /** Standalone SVG for one page (fonts converted to outlines). */\n toSvg(displayList: DisplayList, opts?: SvgOptions): Promise<string>;\n\n /** Run layout in a dedicated worker. */\n createLayoutWorker(opts?: LayoutWorkerOptions): LayoutWorker;\n /** Cached tile painter for large unpaged layouts. */\n createTileRenderer(): TileRenderer;\n}\n\nlet enginePromise: Promise<Engine> | null = null;\nlet loadedEngine: Engine | null = null;\n\n/**\n * Load the WASM engine and (by default) its fonts. Concurrent calls share one\n * load; a failed load rejects with `EngineLoadError` and may be retried.\n */\nexport function loadEngine(opts: LoadEngineOptions = {}): Promise<Engine> {\n const base = opts.assetBaseUrl ?? bundledAssetBase();\n if (base) setAssetBasePath(base);\n if (enginePromise) return enginePromise;\n const attempt = loadOnce(opts.textFont !== false).then(\n (engine) => {\n loadedEngine = engine;\n return engine;\n },\n (err: unknown) => {\n if (enginePromise === attempt) enginePromise = null;\n throw err;\n },\n );\n enginePromise = attempt;\n return attempt;\n}\n\n/** True once {@link loadEngine} has resolved. */\nexport function isEngineReady(): boolean {\n return loadedEngine !== null;\n}\n\nasync function loadOnce(textFont: boolean): Promise<Engine> {\n await initWasm();\n if (!isWasmReady()) {\n const cause = getWasmInitError();\n throw new EngineLoadError(\n `Failed to initialize WASM engine: ${messageOf(cause ?? \"unknown error\")}`,\n \"wasm\",\n cause,\n );\n }\n let failed: readonly string[];\n try {\n ({ failed } = await loadMusicFont({ textFont }));\n } catch (err) {\n throw new EngineLoadError(`Failed to load fonts: ${messageOf(err)}`, \"font\", err);\n }\n if (failed.includes(\"Bravura\")) {\n throw new EngineLoadError(\"Failed to load the Bravura music font\", \"font\");\n }\n return createEngine();\n}\n\nfunction absoluteAssetBase(): string {\n const base = resolveBasePath();\n if (typeof document === \"undefined\") return base;\n return new URL(base, document.baseURI).href;\n}\n\n/** Build the engine facade. Requires WASM only for layout, info and SVG. */\nexport function createEngine(): Engine {\n const version: EngineVersion = {\n engine: getEngineVersion() ?? \"unknown\",\n package: PACKAGE_VERSION,\n commit: buildCommit(),\n };\n\n return {\n version,\n\n layout(mnx, opts) {\n const args = layoutArgs(mnx, opts);\n try {\n return wrapDisplayList(\n wasmComputeMnxScoreLayout(args.json, args.spatium, args.pageWidth, args.scoreIndex, args.pageSetupJson),\n );\n } catch (err) {\n throw layoutFailure(err);\n }\n },\n\n paint(ctx, displayList, opts = {}) {\n paintInk(ctx, unwrapDisplayList(displayList), opts);\n },\n\n measure: (displayList) => measureLayout(unwrapDisplayList(displayList)),\n systems: (displayList) => systemGeometry(unwrapDisplayList(displayList)),\n measures: (displayList) => measureGeometry(unwrapDisplayList(displayList)),\n horizonPaper: (displayList) => horizonPaper(unwrapDisplayList(displayList)),\n positionToCanvas: (displayList, position) => positionToCanvas(unwrapDisplayList(displayList), position),\n playhead: (displayList, position) => playheadAt(unwrapDisplayList(displayList), position),\n canvasToBeat: (displayList, page, x, y) => canvasToBeat(unwrapDisplayList(displayList), page, x, y),\n\n info(mnx) {\n const json = mnxJson(mnx);\n try {\n const raw = getScoreInfo(json);\n return {\n parts: raw.partIds.map((id, index) => ({ id, index, name: raw.partNames[index] ?? \"\" })),\n measureCount: raw.measureCount,\n scores: raw.scoreNames.map((name, index) => ({ index, name })),\n };\n } catch (err) {\n throw new ParseError(`Failed to read score info: ${messageOf(err)}`, \"schema\", err);\n }\n },\n\n timeline(mnx, opts = {}) {\n try {\n return buildTimeline(mnx, opts);\n } catch (err) {\n if (err instanceof ParseError) throw err;\n throw new ParseError(`Failed to build timeline: ${messageOf(err)}`, \"schema\", err);\n }\n },\n\n toSvg(displayList, opts = {}) {\n return exportPageSvg(unwrapDisplayList(displayList), absoluteAssetBase(), opts);\n },\n\n createLayoutWorker: (opts) => createLayoutWorker(absoluteAssetBase(), opts),\n createTileRenderer,\n };\n}\n", "/**\n * @viritura/renderer \u2014 Canvas 2D score renderer.\n */\n\nexport { ScoreRenderer } from \"./ScoreRenderer\";\nexport { renderScore } from \"./renderScore\";\nexport { paintDisplayList, paintCommand, loadMusicFont } from \"./displayListPainter\";\nexport type { FontLoadResult, LoadMusicFontOptions } from \"./displayListPainter\";\nexport { canvasFontFamily, canvasTextFont, TEXT_FONT_FAMILY } from \"./textFont\";\nexport {\n decodeBinaryDisplayList,\n paintBinaryDisplayList,\n getBinaryDisplayListDimensions,\n getBinaryDisplayListPages,\n getBinaryDisplayListBboxes,\n} from \"./binaryDisplayList\";\nexport { decodeFrame, decodePatchFrame, PatchReconstructor } from \"./patchFrame\";\nexport type { DecodedFrame, PatchFrame, Placement } from \"./patchFrame\";\nexport { GlyphAtlas, COMMON_GLYPHS } from \"./glyphAtlas\";\nexport type { GlyphAtlasConfig } from \"./glyphAtlas\";\nexport { PageCache, splitCommandsByPage } from \"./pageCache\";\nexport {\n SpatialIndex,\n paintSelectionOverlay,\n paintMeasureSelectionOverlay,\n paintHitboxDebug,\n paintHitboxDebugLegend,\n getElementType,\n hitTestSpannerHandle,\n paintSpannerDragPreview,\n} from \"./hitTest\";\nexport { getHitboxDebugKind, HITBOX_DEBUG_STYLES } from \"./hitboxDebug\";\nexport type { HitboxDebugBounds, HitboxDebugKind, HitboxDebugOptions } from \"./hitboxDebug\";\nexport { hitTestBeamInk, isBeamElementId, selectionGroupMembers } from \"./beamHitTest\";\nexport type { ElementBBox, ScoreElementType, SpannerHandleHit, SpannerHandleEnd, DragSnapPoint } from \"./hitTest\";\nexport { paintBeatRuler } from \"./beatRuler\";\nexport type { RulerTick, RulerConfig } from \"./beatRuler\";\nexport {\n initWasm,\n isWasmReady,\n getWasmInitError,\n getEngineVersion,\n computeLayout as wasmComputeLayout,\n computeFullScoreLayout as wasmComputeFullScoreLayout,\n computeLayoutBinary as wasmComputeLayoutBinary,\n computeFullScoreLayoutBinary as wasmComputeFullScoreLayoutBinary,\n computeMnxScoreLayout as wasmComputeMnxScoreLayout,\n computeMnxScoreLayoutBinary as wasmComputeMnxScoreLayoutBinary,\n computeSlurPreview as wasmComputeSlurPreview,\n computeNotePreview as wasmComputeNotePreview,\n getScoreInfo,\n wasmExportSvg,\n createCachedLayoutEngine,\n setEmitLayoutDebug,\n getEmitLayoutDebug,\n EMPTY_LAYOUT_METRICS,\n setWasmTiming,\n} from \"./wasm\";\nexport { createLayoutService } from \"./worker\";\nexport type { LayoutService, AsyncCachedLayoutEngine, LayoutWorkerApi } from \"./worker\";\nexport type {\n DisplayList,\n PartSummary,\n RenderCommand,\n ScoreInfo,\n PageLayout,\n BoundingBox,\n MeasureBounds,\n SelectionGroup,\n SlurGeometry,\n SlurPreview,\n SlurPreviewInput,\n NotePreviewInput,\n WasmSvgPage,\n CachedLayoutEngine,\n LayoutDebugInfo,\n PageTurnWarning,\n SystemDebug,\n AboveBreakdown,\n BelowBreakdown,\n MeasureExtreme,\n MeasureSpacing,\n StaffPairDebug,\n GapInfo,\n PlacementDebug,\n LayoutMetrics,\n} from \"./wasm\";\nexport {\n detectStaves,\n detectHorizonStaves,\n detectStavesForViewMode,\n findStaffAtPosition,\n snapToStaffPosition,\n getStaffPosition,\n paintInputCursor,\n paintGhostNote,\n paintGhostClef,\n extractStickyClefInfo,\n paintStickyClefs,\n paintMeasureNumber,\n} from \"./overlayPainter\";\nexport type { StaffInfo, GhostNoteOptions, GhostClefOptions, StickyClefInfo } from \"./overlayPainter\";\nexport {\n PerfTracker,\n isPerfEnabled,\n enablePerfOverlay,\n disablePerfOverlay,\n isTileCacheDisabled,\n disableTileCache,\n enableTileCache,\n getGlobalPerfTracker,\n} from \"./perfOverlay\";\nexport type { PatchInfo } from \"./perfOverlay\";\nexport { TileCache, DEFAULT_TILE_SIZE, computeDisplayListContentBounds, paintCommandsCulled } from \"./tileCache\";\nexport {\n computeHorizonPaperGeometry,\n paintPaperPage,\n HORIZON_PAPER_PADDING,\n PAPER_SHADOW_MARGIN,\n} from \"./paperPagePainter\";\nexport type { HorizonPaperGeometry } from \"./paperPagePainter\";\nexport { beatToX, paintPlayhead, paintPlayheadAtPosition, findSystemYExtent, findPageForY } from \"./playheadPainter\";\nexport type { PlayheadPosition, PlayheadDraw } from \"./playheadPainter\";\nexport { exportPdf } from \"./pdfPainter\";\nexport type { PdfExportOptions } from \"./pdfPainter\";\nexport { exportSvg, exportSvgPages } from \"./svgPainter\";\nexport type { SvgExportOptions, SvgPage } from \"./svgPainter\";\nexport { resolveBasePath, setAssetBasePath } from \"./basePath\";\n", "import type { NoteValueBase } from \"./enums\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Staff & spacing constants\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Number of staff lines in a standard staff */\nexport const STAFF_LINES = 5;\n\n/** Default spatium (staff space) in pixels at 100% zoom */\nexport const DEFAULT_SPATIUM_PX = 10;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration values (in quarter-note beats)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Map from NoteValueBase to beats (quarter note = 1). Covers the full MNX\n * spec range, from `duplexMaxima` (64 beats) down to `4096th`. */\nexport const DURATION_BEATS: Record<NoteValueBase, number> = {\n duplexMaxima: 64,\n maxima: 32,\n longa: 16,\n breve: 8,\n whole: 4,\n half: 2,\n quarter: 1,\n eighth: 0.5,\n \"16th\": 0.25,\n \"32nd\": 0.125,\n \"64th\": 0.0625,\n \"128th\": 0.03125,\n \"256th\": 0.015625,\n \"512th\": 0.0078125,\n \"1024th\": 0.00390625,\n \"2048th\": 0.001953125,\n \"4096th\": 0.0009765625,\n};\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MIDI pitch mapping\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Semitone offsets for each step (C=0, D=2, E=4, F=5, G=7, A=9, B=11) */\nexport const STEP_SEMITONES: Record<string, number> = {\n C: 0,\n D: 2,\n E: 4,\n F: 5,\n G: 7,\n A: 9,\n B: 11,\n};\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Key signature sharps/flats\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Key signature fifths order for sharps */\nexport const SHARP_ORDER: string[] = [\"F\", \"C\", \"G\", \"D\", \"A\", \"E\", \"B\"];\n\n/** Key signature fifths order for flats */\nexport const FLAT_ORDER: string[] = [\"B\", \"E\", \"A\", \"D\", \"G\", \"C\", \"F\"];\n", "import type { NoteValueBase, StemDirection } from \"../enums\";\nimport type {\n AccidentalEnclosure as RawAccidentalEnclosure,\n AccidentalDisplay as RawAccidentalDisplay,\n Written as RawWritten,\n Note as RawNote,\n Staccato as RawStaccato,\n Staccatissimo as RawStaccatissimo,\n Spiccato as RawSpiccato,\n Tenuto as RawTenuto,\n Accent as RawAccent,\n StrongAccent as RawStrongAccent,\n SoftAccent as RawSoftAccent,\n StressMarking as RawStress,\n UnstressMarking as RawUnstress,\n TremoloSingle as RawTremolo,\n BowDirection as RawBowDirection,\n Fermata as RawFermata,\n Arpeggio as RawArpeggio,\n} from \"../raw\";\nimport type { Pitch } from \"./pitch\";\nimport type { KitNote, NoteheadShape } from \"./kit\";\nimport type { Narrow, HoistVendor } from \"./_derive\";\n\n/**\n * Duration (MNX note value).\n */\nexport interface Duration {\n /** Base note value type */\n base: NoteValueBase;\n /** Number of augmentation dots */\n dots?: number;\n}\n\n/**\n * Tie reference (MNX `tie`). Derived directly from MNX raw \u2014 the decoded\n * shape is identical to the wire shape (target id, targetType enum, side,\n * lv flag, plus global-attrs).\n */\nexport type Tie = import(\"../raw\").Tie;\n\n/**\n * Accidental enclosure symbol (MNX accidental-enclosure-symbol).\n * Derived: MNX raw `accidental-enclosure-symbol` exactly.\n */\nexport type AccidentalEnclosureSymbol = RawAccidentalEnclosure[\"symbol\"];\n\n/**\n * Accidental enclosure (MNX accidental-enclosure). Derived from MNX raw.\n */\nexport type AccidentalEnclosure = RawAccidentalEnclosure;\n\n/**\n * Accidental display control (MNX accidentalDisplay). Derived from MNX raw.\n */\nexport type AccidentalDisplay = RawAccidentalDisplay;\n\n/**\n * Written pitch information for transposed scores (MNX `written`).\n * Derived from MNX raw.\n */\nexport type Written = RawWritten;\n\n/**\n * A single note within an event (a chord can have multiple notes).\n * Derived from MNX raw `note`, with `pitch` narrowed to our `Pitch` type\n * (raw uses an open `octave: number`; we narrow to `Octave = 0..9`).\n *\n * `notehead` is a Viritura vendor extension (`_x.viritura.notehead`) \u2014 MNX has\n * no notehead field on `note` (W3C MNX issue #249). It overrides the default\n * notehead for a single pitched note (e.g. a diamond harmonic inside an\n * otherwise normal chord). Stored per-note because chords mix noteheads.\n */\nexport type Note = HoistVendor<Narrow<RawNote, { pitch: Pitch }>, { notehead?: NoteheadShape }>;\n\n/** Staccato marking. Derived from MNX raw `staccato`. */\nexport type Staccato = RawStaccato;\n\n/** Staccatissimo (wedge staccato) marking. Derived from MNX raw. */\nexport type Staccatissimo = RawStaccatissimo;\n\n/** Staccatissimo wedge variant (Viritura extension; not in MNX spec). */\nexport interface StaccatissimoWedge {\n placement?: Placement;\n}\n\n/** Spiccato marking (staccatissimo stroke, SMuFL U+E4AA). Derived from MNX raw. */\nexport type Spiccato = RawSpiccato;\n\n/** Tenuto marking. Derived from MNX raw. */\nexport type Tenuto = RawTenuto;\n\n/** Accent marking (MNX `accent`). Has only `placement` per MNX v15 spec. */\nexport type Accent = RawAccent;\n\n/** Strong accent (marcato) marking. Derived from MNX raw. */\nexport type StrongAccent = RawStrongAccent;\n\n/** Soft accent (Bart\u00F3k accent) marking \u2014 a hairpin-like wedge (<>). Derived from MNX raw. */\nexport type SoftAccent = RawSoftAccent;\n\n/** Stress marking. Derived from MNX raw `stress-marking`. */\nexport type Stress = RawStress;\n\n/** Unstress marking. Derived from MNX raw `unstress-marking`. */\nexport type Unstress = RawUnstress;\n\n/** Single-note tremolo marking (1\u20133 slashes on the stem). Derived from MNX raw `tremolo-single`. */\nexport type Tremolo = RawTremolo;\n\n/**\n * Bow direction marking (MNX `bowDirection`). Derived from MNX raw.\n * `direction` (required): up = upbow (V), down = downbow (\u2293).\n */\nexport type BowDirection = RawBowDirection;\n\n/** Standard MNX breath-mark symbol type. */\nexport type BreathMarkSymbol = \"comma\" | \"tick\" | \"upbow\" | \"salzedo\" | \"auto\";\n\n/** Breath mark \u2014 indicates a breathing point between notes. */\nexport interface BreathMark {\n /** Symbol style. Omitted and `auto` both leave the choice to the engraver. */\n symbol?: BreathMarkSymbol;\n placement?: Placement;\n}\n\n/** Arpeggio marking \u2014 wavy line to the left of a chord.\n * Derived from MNX raw `arpeggio` (note-level marking). The decoded\n * model only uses `direction`; `arrow`, `position`, `span` live on the\n * measure-level {@link PartMeasureArpeggio} instead. */\nexport type Arpeggio = Pick<RawArpeggio, \"direction\">;\n\n/** Arpeggio direction. */\nexport type ArpeggioDirection = RawArpeggio[\"direction\"];\n\n/** Caesura style variants (MNX `caesura-shape`). */\nexport type CaesuraStyle = \"normal\" | \"thick\" | \"short\" | \"curved\";\n\n/** A caesura (break) marking on an event (native MNX since schema v36). */\nexport interface Caesura {\n /** Style variant (default: \"normal\"). */\n style?: CaesuraStyle;\n /** Number of strokes (default: 2). SMuFL only defines a single-stroke\n * glyph (`caesuraSingleStroke`), so `marks: 1` only renders distinctly\n * for the \"normal\"/\"short\" shapes; \"thick\"/\"curved\" fall back to their\n * standard double-stroke glyph regardless of `marks`. */\n marks?: 1 | 2;\n}\n\n/** Fermata visual symbol (MNX `fermata-symbol`). Derived from MNX raw. */\nexport type FermataSymbol = NonNullable<RawFermata[\"symbol\"]>;\n\n/** Fermata pause duration (MNX `fermata-duration`). Derived from MNX raw. */\nexport type FermataDuration = NonNullable<RawFermata[\"duration\"]>;\n\n/** Symbol placement (MNX `placement` \u2014 above/below/auto). */\nexport type Placement = NonNullable<RawStaccato[\"placement\"]>;\n\n/** Up/down/auto direction (MNX `up-down-auto`). */\nexport type UpDownAuto = NonNullable<RawStrongAccent[\"pointing\"]>;\n\n/** Up/down direction (MNX `up-down`). */\nexport type UpDown = RawBowDirection[\"direction\"];\n\n/** Direction hint for a sequence's stem/notation side (MNX `direction-hint`).\n * Omitting the field means `auto`. The hint resolves stem direction only\n * while another sequence in the same part measure engraves visible content. */\nexport type DirectionHint = import(\"../raw\").DirectionHint;\n\n/** Fermata (hold) marking on a note or rest. Derived from MNX raw. */\nexport type Fermata = RawFermata;\n\n/** Wavy trill-extension line ending at another event. */\nexport interface TrillExtension {\n target: string;\n /** Which rhythmic edge of the target event ends the line. Defaults to \"start\". */\n targetEdge?: \"start\" | \"end\";\n}\n\n/** Trill ornament marking, optionally followed by a wavy extension line. */\nexport interface Trill {\n /** Accidental alteration: -1 = flat, 0 = natural, 1 = sharp */\n accidental?: number;\n /** Whether to display the initial trill symbol. Defaults to true. */\n showSymbol?: boolean;\n /** Wavy extension line from this event to a target event. */\n extension?: TrillExtension;\n}\n\n/** Ornament type variants \u2014 aliased to the Viritura vendor `ornament-type` schema. */\nexport type OrnamentType = import(\"../raw/raw-viritura\").OrnamentType;\n\n/** Fingering annotation \u2014 a digit (0\u20135) placed near a notehead. */\nexport interface Fingering {\n /** Finger number: 0 (thumb in some traditions), 1\u20135. */\n finger: number;\n}\n\n/**\n * Event markings \u2014 articulations and other note-level annotations (MNX markings).\n */\nexport interface Markings {\n staccato?: Staccato;\n staccatissimo?: Staccatissimo;\n staccatissimoWedge?: StaccatissimoWedge;\n spiccato?: Spiccato;\n accent?: Accent;\n tenuto?: Tenuto;\n strongAccent?: StrongAccent;\n softAccent?: SoftAccent;\n stress?: Stress;\n unstress?: Unstress;\n tremolo?: Tremolo;\n breath?: BreathMark;\n /** Bow direction marking (MNX `bowDirection`). */\n bowDirection?: BowDirection;\n trill?: Trill;\n ornaments?: OrnamentType[];\n arpeggio?: Arpeggio;\n /** Caesura (break) marking (native MNX since schema v36). */\n caesura?: Caesura;\n /** Fingering annotations (digits placed near noteheads). */\n fingerings?: Fingering[];\n}\n\n/**\n * MNX \"space\" element \u2014 a rhythmic gap that advances time without rendering.\n */\nexport interface Space {\n type: \"space\";\n /** Duration as a fraction [numerator, denominator] */\n duration: [number, number];\n}\n\n/** Syllable type for a lyric (MNX event-lyric-line-type). */\nexport type LyricLineType = \"start\" | \"middle\" | \"end\" | \"whole\";\n\n/** A single lyric line entry on an event (MNX event-lyric-line). */\nexport interface LyricLine {\n /** The syllable text */\n text: string;\n /** Syllable type indicating word continuation */\n type?: LyricLineType;\n}\n\n/**\n * Lyrics attached to an event (MNX lyrics).\n * The lines map keys are lyric line IDs (e.g., \"1\", \"2\" for verse numbers).\n */\nexport interface Lyrics {\n lines?: Record<string, LyricLine>;\n}\n\n/** Slur line type (MNX lineType). */\nexport type SlurLineType = \"solid\" | \"dashed\" | \"dotted\";\n\n/** Slur side (MNX side / sideEnd). `\"auto\"` and omission both mean automatic placement. */\nexport type SlurSide = \"up\" | \"down\" | \"auto\";\n\n/**\n * Per-slur shape override stored in `_x.viritura.shape`.\n *\n * Each field is a `[dx, dy]` delta in spatia (sp) applied on top of the\n * engine-computed bezier point. Used by engrave-mode handle drags so user\n * edits compose with automatic collision avoidance.\n */\nexport interface SlurShape {\n /** Start endpoint (p0) delta in sp. */\n p0?: [number, number];\n /** First control point (p1) delta in sp. */\n p1?: [number, number];\n /** Second control point (p2) delta in sp. */\n p2?: [number, number];\n /** End endpoint (p3) delta in sp. */\n p3?: [number, number];\n}\n\n/** Slur reference (MNX slur). */\nexport interface Slur {\n target: string;\n side?: SlurSide;\n sideEnd?: SlurSide;\n lineType?: SlurLineType;\n startNote?: string;\n endNote?: string;\n /** Viritura vendor extension: per-handle bezier overrides for engrave mode. */\n shape?: SlurShape;\n}\n\n/** Glissando line style. */\nexport type GlissandoStyle = \"straight\" | \"wavy\";\n\n/** Musical meaning of a glissando-family line, independent of its label. */\nexport type GlissandoKind = \"glissando\" | \"portamento\";\n\n/** Glissando line connecting two notes at different pitches. */\nexport interface Glissando {\n target: string;\n kind?: GlissandoKind;\n style?: GlissandoStyle;\n text?: string;\n showText?: boolean;\n}\n\n/**\n * An event is a single rhythmic moment: a note, chord, or rest.\n * This is the fundamental unit of content in a sequence.\n */\nexport interface NoteEvent {\n type: \"event\";\n /** Unique ID */\n id?: string;\n /** Duration of this event */\n duration: Duration;\n /** Notes in this event (empty/undefined = rest) */\n notes?: Note[];\n /** Drum kit hits in this event (MNX `kitNotes`). Only on percussion parts. */\n kitNotes?: KitNote[];\n /** If present, this is a rest. staffPosition overrides default vertical position. */\n rest?: { staffPosition?: number };\n /** Cross-staff override: render this event on the specified staff number (1-indexed) */\n staff?: number;\n /** Stem direction override */\n stemDirection?: StemDirection;\n /** Slurs starting from this event */\n slurs?: Slur[];\n /** Glissando lines starting from this event */\n glissandos?: Glissando[];\n /** Articulation markings */\n markings?: Markings;\n /** Native MNX fermata (event-level since v15). */\n fermata?: Fermata;\n /** Lyrics attached to this event */\n lyrics?: Lyrics;\n}\n\n/** Grace note type \u2014 how the grace notes affect timing (MNX grace-type). */\nexport type GraceType = \"makeTime\" | \"stealFollowing\" | \"stealPrevious\";\n\n/** A grace note container (MNX grace). */\nexport interface Grace {\n type: \"grace\";\n /** The events within this grace note group */\n content: NoteEvent[];\n /** How the grace notes affect timing */\n graceType?: GraceType;\n /** Whether to display a slash through the flag/beam */\n slash?: boolean;\n /** Optional rendering color (MNX `color`, e.g. \"#ff0000\"). */\n color?: string;\n}\n\n/** Duration specification for a tuplet (inner or outer). */\nexport interface TupletDuration {\n duration: Duration;\n multiple: number;\n}\n\n/** MNX `yes-no-auto` for tuplet bracket display. */\nexport type TupletBracket = \"yes\" | \"no\" | \"auto\";\n\n/** MNX `tuplet-display-setting` for showNumber / showValue. */\nexport type TupletDisplaySetting = \"noNumber\" | \"inner\" | \"both\";\n\n/** Links measure-local tuplet fragments into one cross-barline tuplet. */\nexport interface TupletSpan {\n id: string;\n type: \"start\" | \"continue\" | \"stop\";\n}\n\n/** A tuplet container (MNX tuplet). */\nexport interface Tuplet {\n type: \"tuplet\";\n /** Inner duration: what the content actually spans */\n inner: TupletDuration;\n /** Outer duration: the notated duration the tuplet replaces */\n outer: TupletDuration;\n /** The events (or nested tuplets) within this tuplet */\n content: SequenceContent[];\n /** Whether to show the tuplet bracket (MNX `bracket`). Default: auto. */\n bracket?: TupletBracket;\n /** Which number(s) to display (MNX `showNumber`). Default: inner. */\n showNumber?: TupletDisplaySetting;\n /** Which note value(s) to display (MNX `showValue`). Default: absent. */\n showValue?: TupletDisplaySetting;\n /** Vertical placement (MNX `placement`, above/below/auto) for the tuplet\n * bracket/number. */\n placement?: Placement;\n /** Cross-staff tuplet: render on the specified staff number (1-indexed). */\n staff?: number;\n /** Cross-barline relationship (Viritura extension `_x.viritura.span`). */\n span?: TupletSpan;\n}\n\n/** Multi-note tremolo container (MNX tremolo). */\nexport interface MultiNoteTremolo {\n type: \"tremolo\";\n /** The two events in this tremolo */\n content: NoteEvent[];\n /** Number of tremolo slashes (1, 2, or 3) */\n marks: number;\n /** Outer duration: the total notated duration the tremolo occupies */\n outer: TupletDuration;\n /** Individual note duration override (MNX individual-duration). */\n individualDuration?: Duration;\n}\n\n/**\n * Visual duration for a full-measure rest (MNX fullMeasure).\n */\nexport interface FullMeasure {\n /** The visual duration to display (e.g., whole rest) */\n visualDuration: Duration;\n /** Explicit staff position override (MNX staffPosition). 0 = middle line, positive = up. */\n staffPosition?: number;\n /** Native MNX fermata on this meter-independent bar rest. */\n fermata?: Fermata;\n}\n\n/**\n * A sequence is a single voice within a measure (MNX \"sequence\").\n * Multiple sequences in one measure = multiple voices.\n */\nexport interface Sequence {\n /** Content items (events, tuplets, grace notes, spaces) */\n content: SequenceContent[];\n /** Full-measure rest indicator */\n fullMeasure?: FullMeasure;\n /** Staff number for this sequence (1-indexed; used in grand staff / organ parts) */\n staff?: number;\n /** Voice name for this sequence (MNX voice identifier) */\n voice?: string;\n /** Stem/notation-side hint for this sequence (MNX `directionHint`).\n * Omitting the field means `auto`. */\n directionHint?: DirectionHint;\n}\n\n/**\n * Content within a sequence \u2014 discriminated union of all content types.\n */\nexport type SequenceContent = NoteEvent | Tuplet | Grace | MultiNoteTremolo | Space;\n\n/**\n * Type guard: narrow SequenceContent to NoteEvent.\n */\nexport function isNoteEvent(item: SequenceContent): item is NoteEvent {\n return item.type === \"event\";\n}\n\n/**\n * Helper: is this event a rest?\n */\nexport function isRest(event: NoteEvent): boolean {\n if (event.rest !== undefined) return true;\n const hasNotes = event.notes && event.notes.length > 0;\n const hasKitNotes = event.kitNotes && event.kitNotes.length > 0;\n return !hasNotes && !hasKitNotes;\n}\n", "import type { Grace, MultiNoteTremolo, NoteEvent, SequenceContent, Tuplet } from \"./event\";\n\n/**\n * The content-item kinds that nest child events: `tuplet`, `grace`, and\n * `tremolo`. These are siblings of leaf events in a sequence's flat `content`\n * list \u2014 the only structural difference is that they carry a child `content`\n * array of their own. Centralizing this set is the single source of truth so\n * that adding a future container type is one edit, not a hunt through every\n * tree-walker.\n */\ntype EventContainer = Tuplet | Grace | MultiNoteTremolo;\n\n/** Single source of truth: does this content item nest child events? */\nfunction isEventContainer(item: SequenceContent): item is EventContainer {\n return item.type === \"tuplet\" || item.type === \"grace\" || item.type === \"tremolo\";\n}\n\n/** The child content array of an event container, or null for leaf items. */\nfunction eventContainerContent(item: SequenceContent): readonly SequenceContent[] | null {\n return isEventContainer(item) ? item.content : null;\n}\n\n/**\n * An event paired with the index path that locates it within a sequence's\n * content tree.\n *\n * `path` is the list of indices from the sequence root down to the event:\n * `[i]` for a top-level event at `content[i]`, `[i, j]` for the event at index\n * `j` inside the container at `content[i]`, and so on for nested tuplets. This\n * is the general location representation \u2014 it does not privilege any one\n * container kind, unlike the legacy per-kind index fields.\n */\nexport interface WalkedEvent {\n event: NoteEvent;\n path: number[];\n}\n\n/**\n * Recursively yield every {@link NoteEvent} in a content array together with\n * its index path. This is the canonical event iterator: every place that needs\n * to \"visit all events, descending into containers\" should delegate here rather\n * than re-implementing the descent (and inevitably forgetting a container kind).\n */\nexport function* walkSequenceEvents(\n content: readonly SequenceContent[],\n prefix: readonly number[] = [],\n): Generator<WalkedEvent> {\n for (let i = 0; i < content.length; i++) {\n const item = content[i]!;\n if (item.type === \"event\") {\n yield { event: item, path: [...prefix, i] };\n continue;\n }\n const inner = eventContainerContent(item);\n if (inner) {\n yield* walkSequenceEvents(inner, [...prefix, i]);\n }\n }\n}\n", "import type { Pitch as RawPitch } from \"../raw\";\nimport type { Step, Octave } from \"../enums\";\nimport type { Narrow } from \"./_derive\";\n\n/**\n * A musical pitch. Derived from MNX raw `pitch`, with `octave` narrowed\n * from raw's open `number` to the finite tuple-union `Octave` (0..9) so\n * exhaustive switches and bounds checks work at compile time.\n *\n * Raw fields preserved:\n * - step: \"A\"|\"B\"|\"C\"|\"D\"|\"E\"|\"F\"|\"G\"\n * - alter?: chromatic alteration in semitones\n */\nexport type Pitch = Narrow<RawPitch, { octave: Octave }>;\n\n/**\n * Compute the diatonic staff position of a pitch relative to a reference.\n * Position 0 = reference pitch, positive = above, negative = below.\n * Each step is 1 position (= half a staff space).\n */\nexport function diatonicPosition(pitch: Pitch): number {\n const stepPositions: Record<string, number> = {\n C: 0,\n D: 1,\n E: 2,\n F: 3,\n G: 4,\n A: 5,\n B: 6,\n };\n return stepPositions[pitch.step]! + pitch.octave * 7;\n}\n\n/**\n * Compute MIDI note number for a pitch.\n */\nexport function pitchToMidi(pitch: Pitch): number {\n const stepSemitones: Record<string, number> = {\n C: 0,\n D: 2,\n E: 4,\n F: 5,\n G: 7,\n A: 9,\n B: 11,\n };\n return (pitch.octave + 1) * 12 + stepSemitones[pitch.step]! + (pitch.alter ?? 0);\n}\n\nconst STEPS_ASCENDING: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"];\n\n/**\n * Build a Pitch result, only including alter when defined.\n */\nfunction makePitch(step: Step, octave: Octave, alter: number | undefined): Pitch {\n const result: Pitch = { step, octave };\n if (alter !== undefined) {\n result.alter = alter;\n }\n return result;\n}\n\n/**\n * Move a pitch up by one diatonic step, preserving the alteration.\n * B4 \u2192 C5 (crosses octave boundary).\n */\nexport function stepPitchUp(pitch: Pitch): Pitch {\n const idx = STEPS_ASCENDING.indexOf(pitch.step);\n if (idx === 6) {\n const nextOctave = (pitch.octave + 1) as Octave;\n const clamped = (nextOctave > 9 ? 9 : nextOctave) as Octave;\n return makePitch(\"C\", clamped, pitch.alter);\n }\n return makePitch(STEPS_ASCENDING[idx + 1]!, pitch.octave, pitch.alter);\n}\n\n/**\n * Move a pitch down by one diatonic step, preserving the alteration.\n * C4 \u2192 B3 (crosses octave boundary).\n */\nexport function stepPitchDown(pitch: Pitch): Pitch {\n const idx = STEPS_ASCENDING.indexOf(pitch.step);\n if (idx === 0) {\n const prevOctave = (pitch.octave - 1) as Octave;\n const clamped = (prevOctave < 0 ? 0 : prevOctave) as Octave;\n return makePitch(\"B\", clamped, pitch.alter);\n }\n return makePitch(STEPS_ASCENDING[idx - 1]!, pitch.octave, pitch.alter);\n}\n\nconst STEPS: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"] as const;\n\n/**\n * Construct a Pitch from its diatonic position (C0=0, D0=1, \u2026, B0=6, C1=7, \u2026).\n * Handles negative diatonic positions correctly.\n * @param alter Optional chromatic alteration in semitones.\n */\nexport function pitchFromDiatonic(diatonic: number, alter?: number): Pitch {\n const octave = Math.floor(diatonic / 7) as Octave;\n const stepIndex = ((diatonic % 7) + 7) % 7; // safe modulo for negatives\n const step = STEPS[stepIndex]!;\n return alter !== undefined ? { step, octave, alter } : { step, octave };\n}\n", "import type { Time as RawTime } from \"../raw\";\nimport type { HoistVendor, Narrow } from \"./_derive\";\n\n/**\n * How a meter's beat grouping is presented, independent of the semantic\n * {@link TimeSignatureExtensions.beatStructure} it is derived from.\n *\n * `\"standard\"` engraves an ordinary numeric (or symbolic) meter with no\n * grouping decoration. `\"additive\"` and `\"annotation\"` are only ever\n * engraved for a meter whose resolved beat structure is structurally\n * non-default and has more than one group; see {@link resolveGroupingDisplay}.\n */\nexport type GroupingDisplay = \"standard\" | \"additive\" | \"annotation\";\n\nexport interface TimeSignatureExtensions {\n /** Ordered beat-group lengths in units of the time-signature denominator. */\n beatStructure?: number[];\n /**\n * Explicit per-occurrence grouping-display override for this time\n * signature. Forces the named mode regardless of the document's house\n * style, subject only to the symbolic-display/single-group safety\n * fallback in {@link resolveGroupingDisplay}.\n */\n groupingDisplay?: GroupingDisplay;\n}\n\n/**\n * Time signature. Derived from MNX raw `time`, with the `display` enum\n * extended to cover `\"senzaMisura\"` and `\"note\"` \u2014 Viritura-specific\n * display modes the parser produces from vendor extensions.\n *\n * Field naming preserved: raw `count` (numerator), `unit` (denominator).\n */\nexport type TimeSignature = HoistVendor<\n Narrow<\n RawTime,\n {\n /** Beat unit: 1, 2, 4, 8, 16, 32, 64, 128. Kept as `number` rather than\n * the raw narrow union so editor/UI callers that compute units\n * numerically type-check without per-call widening casts. */\n unit: number;\n display?: \"common\" | \"cut\" | \"senzaMisura\" | \"note\";\n }\n >,\n TimeSignatureExtensions\n>;\n\n/**\n * Compute the total number of quarter-note beats in a measure with this time sig.\n */\nexport function measureBeats(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\nexport interface ResolvedMeter {\n count: number;\n unit: number;\n /** Ordered beat-group lengths in denominator units. */\n beatStructure: number[];\n /** Beat-group boundaries in quarter-note beats, including measure start and end. */\n beatBoundaries: number[];\n source: \"authored\" | \"default\";\n}\n\nconst IRREGULAR_EIGHTH_DEFAULTS: Readonly<Record<number, readonly number[]>> = {\n 5: [3, 2],\n 7: [2, 2, 3],\n 8: [3, 3, 2],\n};\n\nexport function defaultBeatStructure(count: number, unit: number): number[] {\n assertValidTimeSignature(count, unit);\n const irregularShortMeter = unit >= 8 ? IRREGULAR_EIGHTH_DEFAULTS[count] : undefined;\n if (irregularShortMeter) return [...irregularShortMeter];\n if (count % 3 === 0 && (unit >= 8 || count > 3)) {\n return Array.from({ length: count / 3 }, () => 3);\n }\n if (unit >= 8) {\n const quarterUnits = unit / 4;\n const groups: number[] = [];\n for (let remaining = count; remaining > 0; remaining -= quarterUnits) {\n groups.push(Math.min(quarterUnits, remaining));\n }\n return groups;\n }\n return Array.from({ length: count }, () => 1);\n}\n\nexport function resolveMeter(time: TimeSignature): ResolvedMeter {\n assertValidTimeSignature(time.count, time.unit);\n\n const authored = time.beatStructure;\n if (\n authored &&\n (authored.length === 0 ||\n authored.some((group) => !Number.isInteger(group) || group <= 0) ||\n authored.reduce((sum, group) => sum + group, 0) !== time.count)\n ) {\n throw new RangeError(\"Time-signature beatStructure must contain positive integers that sum to count\");\n }\n\n const beatStructure = authored ? [...authored] : defaultBeatStructure(time.count, time.unit);\n const beatBoundaries = [0];\n for (const group of beatStructure) {\n beatBoundaries.push(beatBoundaries[beatBoundaries.length - 1]! + (group * 4) / time.unit);\n }\n return {\n count: time.count,\n unit: time.unit,\n beatStructure,\n beatBoundaries,\n source: authored ? \"authored\" : \"default\",\n };\n}\n\nfunction assertValidTimeSignature(count: number, unit: number): void {\n if (!Number.isInteger(count) || count <= 0) {\n throw new RangeError(\"Time-signature count must be a positive integer\");\n }\n if (![1, 2, 4, 8, 16, 32, 64, 128].includes(unit)) {\n throw new RangeError(\"Time-signature unit must be a supported power-of-two denominator\");\n }\n}\n\n/**\n * Whether a resolved beat structure differs from the meter's conventional\n * default \u2014 i.e. whether it is worth decorating at all. An authored\n * structure that happens to match the automatic default (a \"redundant\"\n * structure) does not count as non-default.\n */\nfunction isNonDefaultGrouping(resolved: ResolvedMeter): boolean {\n const defaults = defaultBeatStructure(resolved.count, resolved.unit);\n return (\n resolved.beatStructure.length !== defaults.length ||\n resolved.beatStructure.some((group, index) => group !== defaults[index])\n );\n}\n\n/**\n * Resolve which {@link GroupingDisplay} mode a rendered meter should\n * actually engrave.\n *\n * Precedence: an explicit staff occurrence override wins outright, then the\n * time signature's own occurrence override (`time.groupingDisplay`), then\n * the document's non-default house style \u2014 but only when `resolved` is\n * structurally non-default (its beat structure differs from the meter's\n * conventional default). Ordinary/default meters always stay `\"standard\"`\n * under the house style, so a plain 4/4 never engraves `\"1+1+1+1\"`.\n *\n * A symbolic display (`common`/`cut`/`senzaMisura`/`note`) or a resolved\n * beat structure of one group has nothing for additive/annotation to show,\n * so both fall back to `\"standard\"` unconditionally \u2014 even under an\n * explicit occurrence or staff override.\n */\nexport function resolveGroupingDisplay(\n time: TimeSignature,\n resolved: ResolvedMeter,\n houseStyle: GroupingDisplay,\n staffOverride?: GroupingDisplay,\n): GroupingDisplay {\n if (time.display || resolved.beatStructure.length <= 1) {\n return \"standard\";\n }\n if (staffOverride) return staffOverride;\n if (time.groupingDisplay) return time.groupingDisplay;\n if (houseStyle !== \"standard\" && isNonDefaultGrouping(resolved)) {\n return houseStyle;\n }\n return \"standard\";\n}\n", "/**\n * Staff-local synchronous meters (`_x.viritura.staffMeters` on a part\n * measure).\n *\n * A staff-local meter lets one staff of a part notate its own time\n * signature while every staff's barlines stay locked to the shared global\n * measure grid \u2014 non-aligning/independent polymeter is explicitly out of\n * scope (see `docs/spec/viritura-extensions.md`).\n *\n * Two synchronization modes reconcile a staff-local meter's duration with\n * the global measure it shares a barline with:\n *\n * - `\"sharedDuration\"`: the staff-local measure duration must equal the\n * global measure duration exactly (e.g. local 6/8 vs. global 3/4 \u2014 both\n * six quarter-note-equivalent beats). Written note durations need no\n * scaling.\n * - `\"fitMeasure\"`: one complete staff-local measure maps onto one complete\n * global measure by a derived exact ratio (e.g. local 6/8 vs. global\n * 2/4 \u2014 two dotted-quarter pulses mapped onto two quarter-note pulses,\n * ratio 2/3).\n *\n * {@link resolveStaffMeterChange} is the single-step algorithm; declarations\n * inherit until changed or reset, so {@link resolveStaffMeterTimeline} walks\n * an ordered run of part measures carrying that inherited state forward.\n * Mirrors the Rust engine's `model::staff_meter` module.\n */\nimport { measureBeats, resolveMeter, type TimeSignature } from \"./time\";\n\n/** A staff-local meter distinct from the global meter. */\nexport interface StaffMeter {\n count: number;\n unit: number;\n /** Ordered beat-group lengths in units of `unit`. */\n beatStructure?: number[];\n}\n\n/**\n * How a staff-local meter's measure duration relates to the global measure\n * it shares a barline with.\n */\nexport type StaffMeterSynchronization = \"sharedDuration\" | \"fitMeasure\";\n\n/** Establishes a staff-local meter from this part measure onward. */\nexport interface StaffMeterSet {\n staff: number;\n meter: StaffMeter;\n synchronization: StaffMeterSynchronization;\n}\n\n/** Resets a staff back to following the global meter. */\nexport interface StaffMeterReset {\n staff: number;\n useGlobal: true;\n}\n\n/**\n * One `_x.viritura.staffMeters[]` entry on a part measure: either sets a\n * staff-local meter and its synchronization mode, or resets the staff back\n * to the global meter. Effective from this measure onward until changed or\n * reset again.\n */\nexport type StaffMeterChange = StaffMeterSet | StaffMeterReset;\n\n/** Narrows a {@link StaffMeterChange} to its reset variant. */\nexport function isStaffMeterReset(change: StaffMeterChange): change is StaffMeterReset {\n return \"useGlobal\" in change;\n}\n\n/** An exact rational ratio, kept as integers to avoid float precision loss. */\nexport interface RationalRatio {\n num: number;\n den: number;\n}\n\nfunction gcd(a: number, b: number): number {\n return b === 0 ? a : gcd(b, a % b);\n}\n\nfunction reduceRatio(num: number, den: number): RationalRatio {\n const divisor = gcd(Math.abs(num), Math.abs(den)) || 1;\n return { num: num / divisor, den: den / divisor };\n}\n\n/** Convert a {@link RationalRatio} to a floating-point multiplier. */\nexport function ratioToNumber(ratio: RationalRatio): number {\n return ratio.num / ratio.den;\n}\n\n/**\n * The resolved, currently-active staff-local meter for one staff at a given\n * point in the score (i.e. after applying the most recent `staffMeters`\n * declaration for that staff, inherited across measures until changed or\n * reset).\n */\nexport interface EffectiveStaffMeter {\n staff: number;\n /** The staff-local meter, expressed as a {@link TimeSignature} so callers\n * can reuse `resolveMeter()`/`ResolvedMeter` directly. */\n timeSignature: TimeSignature;\n synchronization: StaffMeterSynchronization;\n /** Exact ratio of one global measure duration to one staff-local measure\n * duration (`global / local`). `1/1` for `sharedDuration` (the two\n * durations are required to be equal); the derived mapping ratio for\n * `fitMeasure`. */\n ratioToGlobal: RationalRatio;\n}\n\n/** Thrown when a `staffMeters` entry cannot be resolved. */\nexport class StaffMeterValidationError extends RangeError {\n constructor(message: string) {\n super(message);\n this.name = \"StaffMeterValidationError\";\n }\n}\n\nfunction durationRatio(global: TimeSignature, local: TimeSignature): RationalRatio {\n // measure_beats() = count * 4 / unit, so the ratio's unit-4 factors\n // cancel: global/local = (global.count * local.unit) / (global.unit * local.count).\n return reduceRatio(global.count * local.unit, global.unit * local.count);\n}\n\n/**\n * Resolve one `staffMeters` entry (or inherited absence of one) against the\n * previously-effective state and the measure's effective global meter.\n *\n * An inherited meter (`change` is `undefined`) is not returned unchanged \u2014\n * its authored local meter and synchronization mode are preserved, but the\n * ratio (and, for `sharedDuration`, the equal-duration validity check) are\n * re-derived against *this* measure's effective global meter. A global\n * meter change partway through an inherited `fitMeasure` or\n * `sharedDuration` run therefore updates every following measure's\n * `ratioToGlobal`/validity, not just the measure the declaration was\n * authored on.\n *\n * @throws {StaffMeterValidationError} when a `Set` change's meter is\n * malformed, or its (authored or inherited) `sharedDuration`\n * synchronization does not actually share the global measure's duration.\n * Callers that want lenient, best-effort resolution across many measures\n * should use {@link resolveStaffMeterTimeline}, which keeps the previous\n * effective state on error instead of throwing.\n */\nexport function resolveStaffMeterChange(\n previous: EffectiveStaffMeter | undefined,\n change: StaffMeterChange | undefined,\n global: TimeSignature,\n): EffectiveStaffMeter | undefined {\n if (!change) {\n if (!previous) return undefined;\n return resolveLocalMeter(previous.staff, previous.timeSignature, previous.synchronization, global);\n }\n if (isStaffMeterReset(change)) return undefined;\n\n const localTs: TimeSignature = {\n count: change.meter.count,\n unit: change.meter.unit,\n ...(change.meter.beatStructure ? { beatStructure: change.meter.beatStructure } : {}),\n };\n return resolveLocalMeter(change.staff, localTs, change.synchronization, global);\n}\n\n/**\n * Validate a staff-local meter and derive its ratio against `global`. Shared\n * by a fresh `Set` declaration and by re-resolving an inherited meter each\n * measure \u2014 both must apply the exact same rules against whatever global\n * meter is currently in force.\n */\nfunction resolveLocalMeter(\n staff: number,\n localTs: TimeSignature,\n synchronization: StaffMeterSynchronization,\n global: TimeSignature,\n): EffectiveStaffMeter {\n try {\n // Validates count/unit and (if authored) that beatStructure sums to count.\n resolveMeter(localTs);\n } catch (error) {\n throw new StaffMeterValidationError(\n `invalid staff-local meter: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n const localBeats = measureBeats(localTs);\n const globalBeats = measureBeats(global);\n if (synchronization === \"sharedDuration\" && Math.abs(localBeats - globalBeats) > 1e-9) {\n throw new StaffMeterValidationError(\n `sharedDuration requires equal measure durations: staff-local meter is ${localBeats} quarter-note beats, global measure is ${globalBeats}`,\n );\n }\n\n return {\n staff,\n timeSignature: localTs,\n synchronization,\n ratioToGlobal: durationRatio(global, localTs),\n };\n}\n\nfunction refreshRetainedMeter(\n previous: EffectiveStaffMeter | undefined,\n global: TimeSignature,\n): EffectiveStaffMeter | undefined {\n if (!previous) return undefined;\n try {\n return resolveStaffMeterChange(previous, undefined, global);\n } catch {\n return previous;\n }\n}\n\n/** One measure/staff whose `staffMeters` declaration failed to resolve. */\nexport interface StaffMeterIssue {\n measureIndex: number;\n staff: number;\n error: StaffMeterValidationError;\n}\n\n/**\n * Walk an ordered run of a part's measures, applying `staffMeters`\n * declarations/resets in order and carrying inherited state forward.\n *\n * Returns one `Map<staff, EffectiveStaffMeter>` per measure (only staves\n * currently overridden are present; an absent staff follows the global\n * meter), plus every issue encountered. An invalid declaration is skipped \u2014\n * the previously-effective state for that staff is kept \u2014 so a single bad\n * entry cannot corrupt every following measure. Every staff still under an\n * inherited (non-reset) declaration is also re-resolved against each\n * measure's own effective global meter, so a global meter change partway\n * through an inherited run updates `ratioToGlobal` (or surfaces a newly\n * broken `sharedDuration` equality) from that measure onward. Mirrors the\n * Rust engine's `resolve_staff_meter_table`.\n * If a rejected replacement coincides with a global meter change, the\n * retained declaration is re-resolved against that new global meter. When\n * the retained declaration is `sharedDuration` and is no longer valid, its\n * last valid effective value is kept and the authored replacement remains\n * the sole issue for that staff and measure.\n */\nexport function resolveStaffMeterTimeline(\n staffMetersPerMeasure: readonly (StaffMeterChange[] | undefined)[],\n globalTimeAtMeasure: readonly TimeSignature[],\n): { table: Map<number, EffectiveStaffMeter>[]; issues: StaffMeterIssue[] } {\n const state = new Map<number, EffectiveStaffMeter>();\n const table: Map<number, EffectiveStaffMeter>[] = [];\n const issues: StaffMeterIssue[] = [];\n\n for (let measureIndex = 0; measureIndex < staffMetersPerMeasure.length; measureIndex++) {\n const changes = staffMetersPerMeasure[measureIndex];\n const global = globalTimeAtMeasure[measureIndex] ?? { count: 4, unit: 4 };\n const changedStaves = new Set<number>();\n\n if (changes) {\n for (const change of changes) {\n changedStaves.add(change.staff);\n const previous = state.get(change.staff);\n try {\n const effective = resolveStaffMeterChange(previous, change, global);\n if (effective) {\n state.set(change.staff, effective);\n } else {\n state.delete(change.staff);\n }\n } catch (error) {\n issues.push({\n measureIndex,\n staff: change.staff,\n error: error instanceof StaffMeterValidationError ? error : new StaffMeterValidationError(String(error)),\n });\n // The authored replacement was rejected, so the retained\n // declaration is still inherited at this measure. Re-resolve it\n // against the current global meter; if that also fails (possible\n // for sharedDuration), retain its last valid state without\n // reporting a second issue for the same staff and measure.\n const retained = refreshRetainedMeter(previous, global);\n if (retained) state.set(change.staff, retained);\n }\n }\n }\n\n // Re-resolve every other currently-active staff against this measure's\n // effective global meter: an inherited fitMeasure ratio (or\n // sharedDuration validity) depends on the global meter in force at THIS\n // measure, not the one in force when the declaration was authored. On\n // error, keep the previously-effective entry (lenient, matches the\n // fresh-declaration error path above) and surface the issue.\n for (const [staff, previous] of state) {\n if (changedStaves.has(staff)) continue;\n try {\n const effective = resolveStaffMeterChange(previous, undefined, global);\n if (effective) state.set(staff, effective);\n } catch (error) {\n issues.push({\n measureIndex,\n staff,\n error: error instanceof StaffMeterValidationError ? error : new StaffMeterValidationError(String(error)),\n });\n }\n }\n\n table.push(new Map(state));\n }\n\n return { table, issues };\n}\n", "/**\n * ID generation \u2014 UUID v7 for score elements.\n *\n * Format: canonical hyphenated UUID v7 (36 chars, e.g.\n * \"01927f8a-3a4c-7c8d-9e2f-1a2b3c4d5e6f\"). 48-bit unix-millisecond prefix\n * + 74 random bits + version/variant nibbles.\n *\n * Why v7 over v4 or short base36:\n * - Time-ordered: lexicographic sort \u2248 creation order. Useful for log\n * correlation, CRDT op debugging, and (eventually) DB index locality\n * on the server tier (Postgres/SQL Server B-trees love sequential keys).\n * - 74 random bits \u2248 1.9e22 space \u2014 collision-free at any musical scale.\n * - Canonical string parses as `Guid` in C# and `uuid::Uuid` in Rust,\n * so the same id flows through the TS editor, Rust engine, and .NET\n * server with no custom format glue.\n *\n * Browser/Node both ship v4 natively (`crypto.randomUUID()`), so v7 is\n * built by hand below; this is ~20 lines of byte layout, not crypto.\n */\n\nimport type { Score } from \"./model/score\";\n\nconst HEX = \"0123456789abcdef\";\n\nfunction toHex(byte: number): string {\n return HEX[(byte >>> 4) & 0xf]! + HEX[byte & 0xf]!;\n}\n\n/**\n * Generate a UUID v7 (canonical hyphenated form, 36 chars).\n * Uses crypto.getRandomValues when available, falls back to Math.random.\n */\nexport function generateId(): string {\n const bytes = new Uint8Array(16);\n if (typeof globalThis.crypto !== \"undefined\" && globalThis.crypto.getRandomValues) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n const ts = Date.now();\n // 48-bit big-endian unix-ms timestamp in bytes 0..5\n bytes[0] = Math.floor(ts / 2 ** 40) & 0xff;\n bytes[1] = Math.floor(ts / 2 ** 32) & 0xff;\n bytes[2] = (ts >>> 24) & 0xff;\n bytes[3] = (ts >>> 16) & 0xff;\n bytes[4] = (ts >>> 8) & 0xff;\n bytes[5] = ts & 0xff;\n // Version 7 in high nibble of byte 6\n bytes[6] = (bytes[6]! & 0x0f) | 0x70;\n // RFC 4122 variant (10xx) in high bits of byte 8\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n let s = \"\";\n for (let i = 0; i < 16; i++) {\n s += toHex(bytes[i]!);\n if (i === 3 || i === 5 || i === 7 || i === 9) s += \"-\";\n }\n return s;\n}\n\n/**\n * Generate a unique ID that doesn't collide with any ID in the given set.\n */\nexport function generateUniqueId(existingIds: ReadonlySet<string>): string {\n let id = generateId();\n while (existingIds.has(id)) {\n id = generateId();\n }\n return id;\n}\n\n/**\n * Collect all IDs from a score into a Set for collision checking.\n */\nexport function collectScoreIds(score: Score): Set<string> {\n const ids = new Set<string>();\n for (const gm of score.global.measures) {\n if (gm.id) ids.add(gm.id);\n }\n for (const part of score.parts) {\n if (part.id) ids.add(part.id);\n for (const pm of part.measures) {\n for (const dynamic of pm.dynamics ?? []) {\n ids.add(dynamic.id);\n }\n for (const seq of pm.sequences) {\n collectContentIds(seq.content, ids);\n }\n }\n }\n return ids;\n}\n\nfunction collectContentIds(\n content: ReadonlyArray<{\n type: string;\n id?: string;\n notes?: ReadonlyArray<{ id?: string }>;\n content?: ReadonlyArray<unknown>;\n }>,\n ids: Set<string>,\n): void {\n for (const item of content) {\n if (item.id) ids.add(item.id);\n if (item.notes) {\n for (const n of item.notes) {\n if (n.id) ids.add(n.id);\n }\n }\n if (item.content && Array.isArray(item.content)) {\n collectContentIds(\n item.content as ReadonlyArray<{\n type: string;\n id?: string;\n notes?: ReadonlyArray<{ id?: string }>;\n content?: ReadonlyArray<unknown>;\n }>,\n ids,\n );\n }\n }\n}\n", "export {\n appendMeasure,\n insertMeasure,\n deleteMeasure,\n setTimeSignature,\n setKeySignature,\n setRepeatStart,\n setMeasureRepeat,\n setBarline,\n setRepeatEnd,\n setClef,\n setEnding,\n setGroupingDisplayOverride,\n setStaffMeter,\n setStaffMeterToGlobal,\n} from \"./measureOps\";\nexport {\n accentSpelling,\n createDynamicGroup,\n createRelativeDynamicGroup,\n dynamicSpelling,\n isSupportedDynamicGlyph,\n type AuthoredDynamicValue,\n} from \"./dynamicGroups\";\nexport {\n CHORDS_PART_ID,\n UNSUPPORTED_CHORD_MESSAGE,\n parseChordSymbolText,\n formatChordSymbolText,\n rewriteChordSymbolBase,\n resolveChordSymbol,\n transposeChordSymbol,\n voiceChordSymbol,\n compareChordSymbolPositions,\n upsertGlobalChordSymbol,\n mergeGlobalChordSymbols,\n type ChordSymbolResolution,\n type SupportedChordSymbol,\n type ChordSymbolVoicing,\n type ChordSymbolSource,\n type ChordSymbolConflict,\n type ChordSymbolMergeResult,\n} from \"./chordSymbols\";\n", "export { parseChordSymbolText, formatChordSymbolText, rewriteChordSymbolBase } from \"./text\";\nexport {\n CHORDS_PART_ID,\n UNSUPPORTED_CHORD_MESSAGE,\n resolveChordSymbol,\n voiceChordSymbol,\n type ChordSymbolResolution,\n type SupportedChordSymbol,\n type ChordSymbolVoicing,\n} from \"./resolution\";\nexport { transposeChordSymbol } from \"./transpose\";\nexport {\n compareChordSymbolPositions,\n upsertGlobalChordSymbol,\n mergeGlobalChordSymbols,\n type ChordSymbolSource,\n type ChordSymbolConflict,\n type ChordSymbolMergeResult,\n} from \"./global\";\n", "import type { ChordQuality, ChordRoot, ChordSymbol, RhythmicPosition } from \"../../model\";\nimport { resolveChordSymbol } from \"./resolution\";\nimport { formatModifiers, parseModifiers, type ChordModifiers } from \"./modifiers\";\n\nfunction parseRoot(step: string, accidental: string): ChordRoot {\n const normalized = accidental.replaceAll(\"\u266D\", \"b\").replaceAll(\"\u266F\", \"#\");\n const alter =\n normalized === \"x\" || normalized === \"\uD834\uDD2A\"\n ? 2\n : normalized === \"\uD834\uDD2B\"\n ? -2\n : [...normalized].reduce((sum, token) => sum + (token === \"#\" ? 1 : -1), 0);\n return alter === 0 ? { step: step.toUpperCase() } : { step: step.toUpperCase(), alter };\n}\n\n/** Captures whitespace, root, suffix, and optional slash root without rewriting authored syntax. */\nexport const CHORD_TEXT_PATTERN =\n /^(\\s*)([A-Ga-g])((?:#+|b+|\u266F+|\u266D+|x|\uD834\uDD2A|\uD834\uDD2B)?)([^/]*?)(?:\\/([A-Ga-g])((?:#+|b+|\u266F+|\u266D+|x|\uD834\uDD2A|\uD834\uDD2B)?))?(\\s*)$/u;\n\nexport function isNoChordText(text: string): boolean {\n return /^(?:NC|N\\.C\\.?)$/i.test(text.trim());\n}\n\nfunction parseExtension(value: string | undefined): ChordSymbol[\"extension\"] {\n switch (value) {\n case \"6\":\n return 6;\n case \"7\":\n return 7;\n case \"9\":\n return 9;\n case \"11\":\n return 11;\n case \"13\":\n return 13;\n default:\n return undefined;\n }\n}\n\ninterface ParsedQuality {\n quality: ChordQuality;\n extension?: ChordSymbol[\"extension\"];\n modifiers?: ChordModifiers;\n}\n\nfunction parseSpecialQuality(text: string): ParsedQuality | undefined {\n if (text === \"\") return { quality: \"major\" };\n if (text === \"\u0394\" || text === \"\u25B3\") return { quality: \"major\", extension: 7 };\n if (text === \"5\") return { quality: \"power\" };\n if (text.toLowerCase() === \"sus\" || text.toLowerCase() === \"sus4\") return { quality: \"suspended4\" };\n if (text.toLowerCase() === \"sus2\") return { quality: \"suspended2\" };\n if (text.toLowerCase() === \"m7b5\") return { quality: \"half-diminished\", extension: 7 };\n const halfDiminished = /^(?:0|\u00F8)(6|7|9|11|13)?$/.exec(text);\n if (halfDiminished) return { quality: \"half-diminished\", extension: parseExtension(halfDiminished[1]) ?? 7 };\n const minorMajor = /^(?:m|min|-)(?:maj|ma|M|\u0394|\u25B3)(6|7|9|11|13)?$/i.exec(text);\n if (minorMajor) return { quality: \"minor-major\", extension: parseExtension(minorMajor[1]) ?? 7 };\n const suspended = /^(6|7|9|11|13)sus([24])?$/i.exec(text);\n if (suspended)\n return {\n quality: suspended[2] === \"2\" ? \"suspended2\" : \"suspended4\",\n extension: parseExtension(suspended[1]),\n };\n const reverseSuspended = /^sus([24])?(6|7|9|11|13)$/i.exec(text);\n if (reverseSuspended)\n return {\n quality: reverseSuspended[1] === \"2\" ? \"suspended2\" : \"suspended4\",\n extension: parseExtension(reverseSuspended[2]),\n };\n return undefined;\n}\n\nfunction parseBaseQuality(text: string): ParsedQuality | undefined {\n const special = parseSpecialQuality(text);\n if (special) return special;\n const match = /^(maj|Maj|MAJ|ma|Ma|M|m|min|Min|MIN|-|dim|Dim|DIM|o|\u00B0|aug|Aug|AUG|\\+|\u0394|\u25B3)?(6|7|9|11|13)?$/.exec(text);\n if (!match) return undefined;\n\n const marker = match[1] ?? \"\";\n const normalizedMarker = marker.toLowerCase();\n const extension = parseExtension(match[2]);\n\n let quality: ChordQuality;\n switch (normalizedMarker) {\n case \"m\":\n quality = marker === \"M\" ? \"major\" : \"minor\";\n break;\n case \"min\":\n case \"-\":\n quality = \"minor\";\n break;\n case \"dim\":\n case \"o\":\n case \"\u00B0\":\n quality = \"diminished\";\n break;\n case \"aug\":\n case \"+\":\n quality = \"augmented\";\n break;\n case \"maj\":\n case \"ma\":\n case \"\u0394\":\n case \"\u03B4\":\n case \"\u25B3\":\n quality = \"major\";\n break;\n default:\n quality = extension === undefined || extension === 6 ? \"major\" : \"dominant\";\n }\n\n return extension === undefined ? { quality } : { quality, extension };\n}\n\nexport function parseQuality(text: string): ParsedQuality | undefined {\n const boundary = text.search(/add|omit|no|\\(/i);\n if (boundary < 0) return parseBaseQuality(text);\n const base = parseBaseQuality(text.slice(0, boundary).trim());\n const modifiers = parseModifiers(text.slice(boundary));\n if (!base || !modifiers) return undefined;\n if (\n base.quality === \"major\" &&\n base.extension === undefined &&\n modifiers.added.length === 1 &&\n modifiers.added[0] === 6 &&\n modifiers.omitted.length === 0\n ) {\n return { quality: \"major\", extension: 6 };\n }\n return { ...base, modifiers };\n}\n\n/**\n * Parse the common chord-symbol shorthand used by inline entry.\n *\n * The returned event is anchored by its containing measure and relative time.\n * Every input, including malformed/empty text and NC, is preserved verbatim.\n * Unsupported suffixes are never interpreted as major. Use resolveChordSymbol\n * before playback or semantic validation.\n */\nexport function parseChordSymbolText(input: string, position: RhythmicPosition): ChordSymbol {\n const match = CHORD_TEXT_PATTERN.exec(input);\n if (!match) return { position, rawText: input };\n\n const root = parseRoot(match[2]!, match[3] ?? \"\");\n const parsedQuality = parseQuality(match[4] ?? \"\");\n\n const chord: ChordSymbol = {\n position,\n rawText: input,\n root,\n quality: parsedQuality?.quality ?? \"other\",\n };\n if (!parsedQuality || parsedQuality.modifiers) chord.kindText = match[4]!;\n if (parsedQuality?.extension !== undefined) chord.extension = parsedQuality.extension;\n\n if (match[5]) {\n chord.bass = parseRoot(match[5], match[6] ?? \"\");\n }\n\n return chord;\n}\n\nexport function formatChordRoot(root: ChordRoot): string {\n const alter = root.alter ?? 0;\n if (!Number.isSafeInteger(alter) || Math.abs(alter) > 128) return `${root.step}(${alter})`;\n return root.step + (alter > 0 ? \"#\".repeat(alter) : \"b\".repeat(-alter));\n}\n\nfunction formatQuality(chord: ChordSymbol): string {\n const extension = chord.extension;\n switch (chord.quality ?? \"major\") {\n case \"major\":\n return extension === undefined ? \"\" : extension === 6 ? \"6\" : `maj${extension}`;\n case \"minor\":\n return `m${extension ?? \"\"}`;\n case \"dominant\":\n return String(extension ?? 7);\n case \"diminished\":\n return `dim${extension ?? \"\"}`;\n case \"half-diminished\":\n return `\u00F8${extension ?? 7}`;\n case \"augmented\":\n return `aug${extension ?? \"\"}`;\n case \"minor-major\":\n return `mMaj${extension ?? 7}`;\n case \"suspended2\":\n return `${extension ?? \"\"}sus2`;\n case \"suspended4\":\n return `${extension ?? \"\"}sus4`;\n case \"power\":\n return extension === undefined ? \"5\" : `5(${extension})`;\n case \"other\":\n return chord.kindText ?? \"?\";\n }\n}\n\nfunction formatRewriteModifiers(kind: ParsedQuality | undefined, raw: ChordSymbol | undefined): string {\n const rawModifiers = raw?.kindText === undefined ? undefined : parseQuality(raw.kindText)?.modifiers;\n return formatModifiers({\n added: [...new Set([...(kind?.modifiers?.added ?? []), ...(rawModifiers?.added ?? [])])].sort((a, b) => a - b),\n omitted: [...new Set([...(kind?.modifiers?.omitted ?? []), ...(rawModifiers?.omitted ?? [])])].sort(\n (a, b) => a - b,\n ),\n });\n}\n\n/**\n * Rewrite only the base harmony, retaining explicit degree operations and display overrides.\n * Unknown or contradictory source harmony is left untouched rather than guessed.\n */\nexport function rewriteChordSymbolBase(\n chord: ChordSymbol,\n base: Partial<Pick<ChordSymbol, \"quality\" | \"extension\">>,\n): ChordSymbol {\n const raw = chord.rawText === undefined ? undefined : parseChordSymbolText(chord.rawText, chord.position);\n const source = chord.root ? chord : { ...chord, ...raw };\n const kind = source.kindText === undefined ? undefined : parseQuality(source.kindText);\n // Other can carry a recognized suffix after an explicit quality edit; keep that edit reversible.\n const understood =\n source.quality === \"other\" && kind ? { ...source, quality: kind.quality, extension: kind.extension } : source;\n if (resolveChordSymbol({ ...understood, textOverride: undefined }).status !== \"supported\") return chord;\n const next = { ...source, extension: understood.extension, ...base };\n if (next.quality === source.quality && next.extension === source.extension) return chord;\n const modifiers = formatRewriteModifiers(kind, raw);\n const currentKind = formatQuality(understood) + modifiers;\n if (resolveChordSymbol({ ...understood, kindText: currentKind, textOverride: undefined }).status !== \"supported\")\n return chord;\n const nextBase = next.quality === \"other\" ? { ...next, quality: understood.quality } : next;\n next.kindText = modifiers || next.quality === \"other\" ? formatQuality(nextBase) + modifiers : undefined;\n next.rawText = undefined;\n next.rawText = formatChordSymbolText({ ...next, textOverride: undefined });\n if (resolveChordSymbol({ ...next, quality: nextBase.quality, textOverride: undefined }).status !== \"supported\")\n return chord;\n return next;\n}\n\n/** Semantic plain-text label; provenance remains stored separately from explicit display overrides. */\nexport function formatChordSymbolText(chord: ChordSymbol): string {\n if (chord.textOverride !== undefined) return chord.textOverride;\n if (chord.rawText !== undefined) {\n if (resolveChordSymbol(chord).status !== \"supported\") return chord.rawText;\n if (!chord.root) chord = parseChordSymbolText(chord.rawText, chord.position);\n }\n if (!chord.root) return \"\";\n const suffix =\n chord.kindText !== undefined && resolveChordSymbol(chord).status === \"unsupported\"\n ? chord.kindText\n : formatQuality(chord) +\n (chord.kindText ? formatModifiers(parseQuality(chord.kindText)?.modifiers ?? { added: [], omitted: [] }) : \"\");\n return formatChordRoot(chord.root) + suffix + (chord.bass ? `/${formatChordRoot(chord.bass)}` : \"\");\n}\n", "import type { ChordQuality, ChordRoot, ChordSymbol } from \"../../model\";\nimport { isNoChordText, parseChordSymbolText, parseQuality } from \"./text\";\nimport { applyModifiers, type ChordModifiers } from \"./modifiers\";\n\n/** Derived playback channel only: its runtime part index is score.parts.length. Never persist a Part. */\nexport const CHORDS_PART_ID = \"viritura:derived:chords\";\nexport const UNSUPPORTED_CHORD_MESSAGE = \"Unsupported chord: cannot play this symbol.\";\n\nexport interface SupportedChordSymbol {\n status: \"supported\";\n rootPitchClass: number;\n bassPitchClass: number;\n /** Sorted, unique chord tones after degree operations, excluding any non-chord slash bass. */\n pitchClasses: number[];\n}\n\nexport type ChordSymbolResolution =\n SupportedChordSymbol | { status: \"silent\" } | { status: \"unsupported\"; message: typeof UNSUPPORTED_CHORD_MESSAGE };\n\nexport interface ChordSymbolVoicing {\n /** Exactly one root/slash note in MIDI 36..47 for supported symbols. */\n leftHand: number[];\n /** All chord tones in MIDI 60..71, sorted and deduplicated. */\n rightHand: number[];\n}\n\nconst NATURAL_PITCHES: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };\nconst TRIADS: Record<Exclude<ChordQuality, \"other\">, readonly number[]> = {\n major: [0, 4, 7],\n minor: [0, 3, 7],\n dominant: [0, 4, 7],\n diminished: [0, 3, 6],\n \"half-diminished\": [0, 3, 6],\n augmented: [0, 4, 8],\n \"minor-major\": [0, 3, 7],\n suspended2: [0, 2, 7],\n suspended4: [0, 5, 7],\n power: [0, 7],\n};\n\nconst unsupported = (): ChordSymbolResolution => ({ status: \"unsupported\", message: UNSUPPORTED_CHORD_MESSAGE });\nconst modulo = (value: number): number => ((value % 12) + 12) % 12;\n\nfunction pitchClass(root: ChordRoot): number | undefined {\n const natural = NATURAL_PITCHES[root.step];\n const alter = root.alter ?? 0;\n return typeof natural === \"number\" && Number.isSafeInteger(alter) ? modulo(natural + modulo(alter)) : undefined;\n}\n\nfunction chordDegrees(\n quality: Exclude<ChordQuality, \"other\">,\n triad: readonly number[],\n extension: ChordSymbol[\"extension\"],\n): Map<number, number[]> {\n const thirdDegree = quality === \"suspended2\" ? 2 : quality === \"suspended4\" ? 4 : 3;\n const degrees = new Map<number, number[]>([\n [1, [0]],\n [5, [triad[triad.length - 1]!]],\n ]);\n if (quality !== \"power\") degrees.set(thirdDegree, [triad[1]!]);\n // A sixth is additive, not a tertian extension: it does not imply a seventh.\n if (extension === 6) degrees.set(6, [9]);\n else if (extension !== undefined) {\n const seventh = quality === \"major\" || quality === \"minor-major\" ? 11 : quality === \"diminished\" ? 9 : 10;\n degrees.set(7, [seventh]);\n if (extension >= 9) degrees.set(9, [2]);\n if (extension >= 11) degrees.set(11, [5]);\n if (extension >= 13) degrees.set(13, [9]);\n }\n return degrees;\n}\n\nfunction resolveBase(chord: ChordSymbol, modifiers?: ChordModifiers): ChordSymbolResolution {\n if (!chord.root) return unsupported();\n const rootPitchClass = pitchClass(chord.root);\n const bassPitchClass = chord.bass ? pitchClass(chord.bass) : rootPitchClass;\n const quality = chord.quality ?? \"major\";\n if (rootPitchClass === undefined || bassPitchClass === undefined || quality === \"other\") return unsupported();\n const triad = TRIADS[quality];\n if (!Array.isArray(triad)) return unsupported();\n const impliedSeventh = quality === \"dominant\" || quality === \"half-diminished\" || quality === \"minor-major\";\n const extension = chord.extension ?? (impliedSeventh ? 7 : undefined);\n if (extension !== undefined && ![6, 7, 9, 11, 13].includes(extension)) return unsupported();\n if (quality === \"power\" && extension !== undefined) return unsupported();\n const intervals = applyModifiers(chordDegrees(quality, triad, extension), modifiers);\n return {\n status: \"supported\",\n rootPitchClass,\n bassPitchClass,\n pitchClasses: [...new Set(intervals.map((interval) => modulo(rootPitchClass + interval)))].sort((a, b) => a - b),\n };\n}\n\nfunction resolveStructured(chord: ChordSymbol): ChordSymbolResolution {\n const base = resolveBase(chord);\n if (chord.kindText === undefined) return base;\n const parsed = parseQuality(chord.kindText);\n if (!parsed) return unsupported();\n const description = { ...chord, quality: parsed.quality, extension: parsed.extension };\n const described = resolveBase(description);\n if (!equivalent(base, described)) return unsupported();\n const result = resolveBase(chord, parsed.modifiers);\n return equivalent(result, resolveBase(description, parsed.modifiers)) ? result : unsupported();\n}\n\nfunction resolveText(text: string, chord: ChordSymbol): ChordSymbolResolution {\n if (isNoChordText(text)) return { status: \"silent\" };\n return resolveStructured(parseChordSymbolText(text, chord.position));\n}\n\nfunction equivalent(a: ChordSymbolResolution, b: ChordSymbolResolution): boolean {\n if (a.status === \"silent\" && b.status === \"silent\") return true;\n return (\n a.status === \"supported\" &&\n b.status === \"supported\" &&\n a.rootPitchClass === b.rootPitchClass &&\n a.bassPitchClass === b.bassPitchClass &&\n a.pitchClasses.length === b.pitchClasses.length &&\n a.pitchClasses.every((pitch, index) => pitch === b.pitchClasses[index])\n );\n}\n\n/**\n * The single musical interpretation for validation, UI and playback.\n * A missing quality on a structured root means major; unknown qualities never do.\n * Raw syntax and display overrides must agree with structured harmony. NC is\n * silent only when it has no contradictory structured fields or display text.\n */\nexport function resolveChordSymbol(chord: ChordSymbol): ChordSymbolResolution {\n let result: ChordSymbolResolution;\n if (chord.root) {\n result = resolveStructured(chord);\n if (chord.rawText !== undefined && !equivalent(result, resolveText(chord.rawText, chord))) return unsupported();\n } else {\n if (\n chord.rawText === undefined ||\n chord.bass ||\n chord.quality ||\n chord.extension !== undefined ||\n chord.kindText !== undefined\n ) {\n return unsupported();\n }\n result = resolveText(chord.rawText, chord);\n }\n if (chord.textOverride !== undefined && !equivalent(result, resolveText(chord.textOverride, chord)))\n return unsupported();\n return result;\n}\n\n/** Unsupported/NC symbols produce no notes; callers display resolver diagnostics separately. */\nexport function voiceChordSymbol(chord: ChordSymbol): ChordSymbolVoicing {\n const result = resolveChordSymbol(chord);\n if (result.status !== \"supported\") return { leftHand: [], rightHand: [] };\n return {\n leftHand: [36 + result.bassPitchClass],\n rightHand: result.pitchClasses.map((pitch) => 60 + pitch),\n };\n}\n", "export interface ChordModifiers {\n added: number[];\n omitted: number[];\n}\n\n/** Degree lists require an explicit operation; bare numeric extensions are not guessed. */\nexport function parseModifiers(text: string): ChordModifiers | undefined {\n const result: ChordModifiers = { added: [], omitted: [] };\n let rest = text.trim().toLowerCase();\n if (!rest) return result;\n let operation: \"added\" | \"omitted\" | undefined;\n let needsDegree = false;\n let afterDegree = false;\n let inGroup = false;\n let afterComma = false;\n while (rest) {\n const marker = /^(add|omit|no)/.exec(rest)?.[0];\n if (marker) {\n if (needsDegree) return undefined;\n operation = marker === \"add\" ? \"added\" : \"omitted\";\n needsDegree = true;\n afterDegree = false;\n afterComma = false;\n rest = rest.slice(marker.length).trimStart();\n continue;\n }\n const degreeText = /^(13|11|[1-79])/.exec(rest)?.[0];\n if (degreeText) {\n const degree = Number(degreeText);\n if (!operation || (operation === \"added\" && [1, 3, 5].includes(degree))) return undefined;\n result[operation].push(degree);\n needsDegree = false;\n afterDegree = true;\n afterComma = false;\n rest = rest.slice(degreeText.length).trimStart();\n continue;\n }\n if (rest[0] === \"(\" && !inGroup && !afterComma) {\n inGroup = true;\n afterDegree = false;\n } else if (rest[0] === \")\" && inGroup && afterDegree) {\n inGroup = false;\n } else if (rest[0] === \",\" && afterDegree && !afterComma) {\n afterComma = true;\n afterDegree = false;\n } else {\n return undefined;\n }\n rest = rest.slice(1).trimStart();\n }\n if (inGroup || needsDegree || !afterDegree || afterComma) return undefined;\n return {\n added: [...new Set(result.added)].sort((a, b) => a - b),\n omitted: [...new Set(result.omitted)].sort((a, b) => a - b),\n };\n}\n\nexport function formatModifiers(modifiers: ChordModifiers): string {\n return (\n modifiers.added.map((degree) => `add${degree}`).join(\"\") +\n modifiers.omitted.map((degree) => `omit${degree}`).join(\"\")\n );\n}\n\nconst ADDED_INTERVALS: Readonly<Record<number, number>> = { 2: 2, 4: 5, 6: 9, 7: 10, 9: 2, 11: 5, 13: 9 };\n\nexport function applyModifiers(degrees: Map<number, number[]>, modifiers?: ChordModifiers): number[] {\n // Additions name only that degree. In particular add7 is the minor seventh,\n // and add9/11/13 do not imply any lower extensions.\n for (const degree of modifiers?.added ?? []) {\n degrees.set(degree, [...(degrees.get(degree) ?? []), ADDED_INTERVALS[degree]!]);\n }\n for (const degree of modifiers?.omitted ?? []) degrees.delete(degree);\n return [...degrees.values()].flat();\n}\n", "/**\n * Generic conversion / parse diagnostics, shared across @viritura/format and\n * @viritura/musicxml. Surfaces lossy decisions (dropped fields, fallback\n * substitutions, schema violations) so callers (UIs, CLIs, tests) can report\n * them. Not coupled to any particular importer's logging convention \u2014 the\n * shape (severity + RFC 6901 pointer + optional code) is general.\n */\n\nexport type DiagnosticSeverity = \"info\" | \"warning\" | \"error\";\n\n/** A single conversion diagnostic. */\nexport interface MnxDiagnostic {\n /** RFC 6901 JSON Pointer to the offending node, e.g. `/parts/0/measures/2`. */\n pointer: string;\n /** Human-readable message. Should describe what was dropped or substituted. */\n message: string;\n severity: DiagnosticSeverity;\n /** Stable code for filtering in tooling (e.g. `\"unsupported-clef\"`). */\n code?: string;\n}\n\n/**\n * Mutable collector passed through importers / parsers. Use {@link emit}\n * for the common case; instances are stack-local \u2014 there's no global state.\n */\nexport class DiagnosticCollector {\n private readonly entries: MnxDiagnostic[] = [];\n\n emit(d: MnxDiagnostic): void {\n this.entries.push(d);\n }\n\n warn(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"warning\", code });\n }\n\n info(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"info\", code });\n }\n\n error(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"error\", code });\n }\n\n /** Returns a snapshot of the collected diagnostics in emission order. */\n all(): readonly MnxDiagnostic[] {\n return this.entries.slice();\n }\n\n get length(): number {\n return this.entries.length;\n }\n}\n\n/**\n * Build a JSON Pointer by appending segments. Segments are escaped per\n * RFC 6901 (`~` \u2192 `~0`, `/` \u2192 `~1`). Numbers are stringified.\n */\nexport function ptr(...segments: Array<string | number>): string {\n if (segments.length === 0) return \"\";\n return \"/\" + segments.map((s) => String(s).replace(/~/g, \"~0\").replace(/\\//g, \"~1\")).join(\"/\");\n}\n\n/**\n * Compose a child pointer onto an existing parent pointer.\n */\nexport function ptrChild(parent: string, ...segments: Array<string | number>): string {\n return parent + ptr(...segments);\n}\n", "/**\n * DisplayList Canvas Painter \u2014 renders RenderCommands from the WASM engine.\n *\n * This replaces the old painter.ts which did its own layout calculation.\n * Now the layout is computed in Rust/WASM and this just paints the commands.\n */\n\nimport type { DisplayList, RenderCommand } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { resolveBasePath } from \"./basePath\";\nimport { canvasTextFont, TEXT_FONT_FAMILY } from \"./textFont\";\n\n/** Outcome of {@link loadMusicFont}: names of faces that failed to load. */\nexport interface FontLoadResult {\n readonly failed: readonly string[];\n}\n\n/** Options for {@link loadMusicFont}. */\nexport interface LoadMusicFontOptions {\n /**\n * Also load the bundled Libertinus Serif text face as `Viritura Serif`.\n * Default true. When false, text falls back to the page's generic `serif`\n * (or a `Viritura Serif` face the host registers itself).\n */\n readonly textFont?: boolean;\n}\n\ninterface FaceSpec {\n readonly name: string;\n readonly create: (basePath: string) => FontFace;\n}\n\nconst MUSIC_FACES: readonly FaceSpec[] = [\n {\n name: \"Bravura\",\n create: (basePath) =>\n new FontFace(\"Bravura\", `url(${basePath}fonts/Bravura.otf)`, {\n // Restrict to SMuFL Private Use Area + Musical Symbols block.\n // Without this, the browser falls back to Bravura for U+266D (\u266D) etc.\n // in text runs, rendering oversized music glyphs that obscure text labels.\n unicodeRange: \"U+E000-F8FF, U+F0000-FFFFF\",\n }),\n },\n];\n\n// Libertinus Serif \u2014 OFL-licensed text font with \u266D\u266F\u266E support. Registered\n// under a private family (see textFont.ts) so embedding pages keep their\n// own generic `serif`; the engine's DrawText { font: \"serif\" } maps to it.\nconst TEXT_FACES: readonly FaceSpec[] = (\n [\n [\"Libertinus Serif\", \"Regular\", {}],\n [\"Libertinus Serif Bold\", \"Bold\", { weight: \"bold\" }],\n [\"Libertinus Serif Italic\", \"Italic\", { style: \"italic\" }],\n [\"Libertinus Serif Bold Italic\", \"BoldItalic\", { weight: \"bold\", style: \"italic\" }],\n ] as const\n).map(([name, file, descriptors]) => ({\n name,\n create: (basePath: string) =>\n new FontFace(TEXT_FONT_FAMILY, `url(${basePath}fonts/LibertinusSerif-${file}.otf)`, descriptors),\n}));\n\n/**\n * Loads a set of faces once. A result is cached only when the required face\n * loaded, so a failed fetch (network, CSP, wrong asset base) can be retried.\n */\nfunction faceGroupLoader(faces: readonly FaceSpec[], required: string | null): () => Promise<FontLoadResult> {\n let loaded = false;\n let pending: Promise<FontLoadResult> | null = null;\n const load = async (): Promise<FontLoadResult> => {\n const basePath = resolveBasePath();\n const results = await Promise.allSettled(faces.map((face) => face.create(basePath).load()));\n const failed: string[] = [];\n for (const [i, result] of results.entries()) {\n if (result.status === \"fulfilled\") {\n document.fonts.add(result.value);\n } else {\n failed.push(faces[i]!.name);\n console.warn(`Failed to load ${faces[i]!.name} font:`, result.reason);\n }\n }\n return { failed };\n };\n return async () => {\n if (loaded) return { failed: [] };\n if (pending) return pending;\n const attempt = load();\n pending = attempt;\n try {\n const result = await attempt;\n if (required === null || !result.failed.includes(required)) loaded = true;\n return result;\n } catch (e) {\n console.warn(\"Failed to load fonts:\", e);\n return { failed: faces.map((face) => face.name) };\n } finally {\n if (!loaded && pending === attempt) pending = null;\n }\n };\n}\n\nconst loadMusicFaces = faceGroupLoader(MUSIC_FACES, \"Bravura\");\nconst loadTextFaces = faceGroupLoader(TEXT_FACES, null);\n\n/**\n * Load the Bravura SMuFL font (always \u2014 layout metrics are Bravura's) and, by\n * default, the Libertinus Serif text font so Canvas can use them.\n */\nexport async function loadMusicFont(opts: LoadMusicFontOptions = {}): Promise<FontLoadResult> {\n const groups = opts.textFont === false ? [loadMusicFaces()] : [loadMusicFaces(), loadTextFaces()];\n const results = await Promise.all(groups);\n return { failed: results.flatMap((result) => result.failed) };\n}\n\n/**\n * Paint a DisplayList onto a Canvas 2D context.\n * Optionally uses a GlyphAtlas for accelerated glyph rendering.\n */\nexport function paintDisplayList(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas?: GlyphAtlas,\n background: string | null = \"#FFFFFF\",\n): void {\n ctx.clearRect(0, 0, displayList.width, displayList.height);\n if (background !== null) {\n ctx.fillStyle = background;\n ctx.fillRect(0, 0, displayList.width, displayList.height);\n }\n\n // Execute each render command\n for (const cmd of displayList.commands) {\n if (cmd.type === \"DrawGlyph\" && glyphAtlas?.isBuilt && cmd.rotation === 0) {\n const drawn = glyphAtlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintCommand(ctx, cmd);\n }\n}\n\ntype CmdOfType<T extends RenderCommand[\"type\"]> = Extract<RenderCommand, { type: T }>;\n\nfunction paintLine(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawLine\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.lineTo(cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\nfunction paintEllipse(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawEllipse\">): void {\n ctx.save();\n ctx.translate(cmd.cx, cmd.cy);\n ctx.rotate(cmd.angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, cmd.rx, cmd.ry, 0, 0, Math.PI * 2);\n if (cmd.filled) {\n ctx.fillStyle = cmd.color;\n ctx.fill();\n } else {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = Math.max(cmd.rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n}\n\nfunction paintText(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawText\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = canvasTextFont(cmd.font, cmd.size);\n ctx.textAlign = cmd.align;\n ctx.textBaseline = cmd.baseline;\n ctx.fillText(cmd.text, cmd.x, cmd.y);\n}\n\nfunction paintGlyph(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawGlyph\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n // Rust's JSON encoding omits zero rotation (binary commands include it).\n if (cmd.rotation != null && cmd.rotation !== 0) {\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.rotate(cmd.rotation);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(cmd.codepoint), cmd.x, cmd.y);\n }\n}\n\nfunction paintStretchedGlyph(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawStretchedGlyph\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.scale(cmd.scale_x, 1);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n}\n\nfunction paintBezier(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawBezier\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.cx1, cmd.cy1, cmd.cx2, cmd.cy2, cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\nfunction paintQuadratic(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawQuadratic\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.quadraticCurveTo(cmd.cx, cmd.cy, cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\n/**\n * Trace the crescent outline of a solid filled-bezier onto `ctx` as the\n * current path, without painting. Shared by the normal painter and the\n * selection highlight so the two can never disagree about the shape.\n *\n * (x1,y1)/(x2,y2) are outer-contour endpoints; (ix1,iy1)/(ix2,iy2)\n * are inner-contour endpoints. Tips are closed with a small quadratic\n * curve that bulges outward along the local tangent, producing the\n * gentle rounded cap of standard engraving practice.\n */\nexport function traceFilledBezier(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n cmd: CmdOfType<\"DrawFilledBezier\">,\n): void {\n const cap1tx = cmd.x1 - cmd.ocx1;\n const cap1ty = cmd.y1 - cmd.ocy1;\n const cap1tl = Math.hypot(cap1tx, cap1ty) || 1;\n const cap1w = Math.hypot(cmd.x1 - cmd.ix1, cmd.y1 - cmd.iy1);\n const cap1ext = cap1w * 0.55;\n const cap1cx = (cmd.x1 + cmd.ix1) / 2 + (cap1tx / cap1tl) * cap1ext;\n const cap1cy = (cmd.y1 + cmd.iy1) / 2 + (cap1ty / cap1tl) * cap1ext;\n\n const cap2tx = cmd.x2 - cmd.ocx2;\n const cap2ty = cmd.y2 - cmd.ocy2;\n const cap2tl = Math.hypot(cap2tx, cap2ty) || 1;\n const cap2w = Math.hypot(cmd.x2 - cmd.ix2, cmd.y2 - cmd.iy2);\n const cap2ext = cap2w * 0.55;\n const cap2cx = (cmd.x2 + cmd.ix2) / 2 + (cap2tx / cap2tl) * cap2ext;\n const cap2cy = (cmd.y2 + cmd.iy2) / 2 + (cap2ty / cap2tl) * cap2ext;\n\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.ocx1, cmd.ocy1, cmd.ocx2, cmd.ocy2, cmd.x2, cmd.y2);\n ctx.quadraticCurveTo(cap2cx, cap2cy, cmd.ix2, cmd.iy2);\n ctx.bezierCurveTo(cmd.icx2, cmd.icy2, cmd.icx1, cmd.icy1, cmd.ix1, cmd.iy1);\n ctx.quadraticCurveTo(cap1cx, cap1cy, cmd.x1, cmd.y1);\n ctx.closePath();\n}\n\nfunction paintFilledBezierSolid(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawFilledBezier\">): void {\n ctx.fillStyle = cmd.color;\n traceFilledBezier(ctx, cmd);\n ctx.fill();\n}\n\n/**\n * Midline (spine) cubic of a filled bezier \u2014 the average of the outer and\n * inner contours. Dashed slurs/ties are stroked along it.\n */\nexport function filledBezierMidline(cmd: CmdOfType<\"DrawFilledBezier\">): {\n x1: number;\n y1: number;\n cx1: number;\n cy1: number;\n cx2: number;\n cy2: number;\n x2: number;\n y2: number;\n thickness: number;\n} {\n return {\n x1: cmd.x1,\n y1: cmd.y1,\n cx1: (cmd.ocx1 + cmd.icx1) / 2,\n cy1: (cmd.ocy1 + cmd.icy1) / 2,\n cx2: (cmd.ocx2 + cmd.icx2) / 2,\n cy2: (cmd.ocy2 + cmd.icy2) / 2,\n x2: cmd.x2,\n y2: cmd.y2,\n thickness: Math.hypot(cmd.ocx1 - cmd.icx1, cmd.ocy1 - cmd.icy1),\n };\n}\n\nfunction paintFilledBezierDashed(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawFilledBezier\">): void {\n const mid = filledBezierMidline(cmd);\n const strokeWidth = Math.max(mid.thickness * 0.5, 1);\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = strokeWidth;\n if (cmd.line_style === 1) {\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (cmd.line_style === 2) {\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(mid.x1, mid.y1);\n ctx.bezierCurveTo(mid.cx1, mid.cy1, mid.cx2, mid.cy2, mid.x2, mid.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\nfunction paintPolygon(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawPolygon\">): void {\n const pts = cmd.points;\n if (pts.length < 3) return;\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n const first = pts[0];\n if (first) ctx.moveTo(first[0], first[1]);\n for (let i = 1; i < pts.length; i++) {\n const pt = pts[i];\n if (pt) ctx.lineTo(pt[0], pt[1]);\n }\n ctx.closePath();\n ctx.fill();\n}\n\nexport function paintCommand(ctx: CanvasRenderingContext2D, cmd: RenderCommand): void {\n switch (cmd.type) {\n case \"DrawLine\":\n paintLine(ctx, cmd);\n break;\n case \"DrawEllipse\":\n paintEllipse(ctx, cmd);\n break;\n case \"DrawRect\":\n ctx.fillStyle = cmd.color;\n ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);\n break;\n case \"DrawCircle\":\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n ctx.arc(cmd.cx, cmd.cy, cmd.r, 0, Math.PI * 2);\n ctx.fill();\n break;\n case \"DrawText\":\n paintText(ctx, cmd);\n break;\n case \"DrawGlyph\":\n paintGlyph(ctx, cmd);\n break;\n case \"DrawStretchedGlyph\":\n paintStretchedGlyph(ctx, cmd);\n break;\n case \"DrawBezier\":\n paintBezier(ctx, cmd);\n break;\n case \"DrawQuadratic\":\n paintQuadratic(ctx, cmd);\n break;\n case \"DrawFilledBezier\":\n if (cmd.line_style === 0 || cmd.line_style === undefined) {\n paintFilledBezierSolid(ctx, cmd);\n } else {\n paintFilledBezierDashed(ctx, cmd);\n }\n break;\n case \"DrawPolygon\":\n paintPolygon(ctx, cmd);\n break;\n case \"SetOpacity\":\n ctx.globalAlpha = cmd.opacity;\n break;\n }\n}\n", "let assetBasePathOverride: string | null = null;\n\n/**\n * Override the static asset base used by renderer consumers that cannot rely\n * on a normal web root, such as VS Code webviews.\n */\nexport function setAssetBasePath(basePath: string | null): void {\n if (!basePath) {\n assetBasePathOverride = null;\n return;\n }\n\n assetBasePathOverride = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n}\n\n/**\n * Resolve the base URL path for fetching static assets (WASM, fonts, scores).\n *\n * Priority:\n * 1. Explicit override set by embedding hosts such as VS Code webviews\n * 2. HTML <base> tag (set by Storybook / Vite when using a subpath)\n * 3. Vite's import.meta.env.BASE_URL (set at build time)\n * 4. Fallback to \"/\"\n *\n * Always returns a path ending with \"/\".\n */\nexport function resolveBasePath(): string {\n if (assetBasePathOverride) {\n return assetBasePathOverride;\n }\n\n // 1. Check for <base href=\"...\"> tag\n if (typeof document !== \"undefined\") {\n const baseEl = document.querySelector(\"base\");\n if (baseEl) {\n const href = baseEl.getAttribute(\"href\");\n if (href) {\n try {\n const base = new URL(href, window.location.origin).pathname;\n return base.endsWith(\"/\") ? base : base + \"/\";\n } catch {\n // ignore malformed base href\n }\n }\n }\n }\n\n // 2. Vite injects BASE_URL at build time\n try {\n const viteBase = (import.meta as { env?: { BASE_URL?: string } }).env?.BASE_URL;\n if (viteBase) {\n return viteBase.endsWith(\"/\") ? viteBase : viteBase + \"/\";\n }\n } catch {\n // import.meta.env may not exist outside Vite\n }\n\n // 3. Default\n return \"/\";\n}\n", "/**\n * Text-font family resolution for render commands.\n *\n * The engine emits `DrawText { font: \"serif\" }` (optionally followed by style\n * words such as `\"serif italic\"`). Libertinus Serif is registered under a\n * private family name so embedding pages keep their own generic `serif`\n * font; the generic family remains the fallback when the private face is not\n * loaded (for example when a host opts out of font loading).\n */\n\n/** Private FontFace family under which Libertinus Serif is registered. */\nexport const TEXT_FONT_FAMILY = \"Viritura Serif\";\n\nconst SERIF_STACK = `\"${TEXT_FONT_FAMILY}\", serif`;\n\n/** Map a render-command family name to a CSS font-family list. */\nexport function canvasFontFamily(family: string): string {\n return family === \"serif\" ? SERIF_STACK : family;\n}\n\n/**\n * Build a Canvas `font` shorthand from a render-command font name, which may\n * carry style words after the family (`\"serif bold italic\"`).\n */\nexport function canvasTextFont(font: string, size: number): string {\n const parts = font.split(\" \");\n const family = canvasFontFamily(parts[0] || \"serif\");\n const style = parts.slice(1).join(\" \");\n return style ? `${style} ${size}px ${family}` : `${size}px ${family}`;\n}\n", "/**\n * Binary Display List decoder and painter.\n *\n * Reads a packed Float32Array produced by the Rust engine's `to_binary()` method\n * and either decodes it into a DisplayList object or paints directly to a Canvas 2D\n * context (for maximum performance).\n *\n * Binary format documentation: see engine/viritura-engine/src/render/binary.rs\n *\n * Per-tag decode/paint routines live in `./binaryDisplayListCommands` so this\n * file's top-level entry points stay within complexity/length budgets.\n */\n\nimport type {\n DisplayList,\n PageLayout,\n RenderCommand,\n ElementBBox,\n BoundingBox,\n SlurGeometry,\n MeasureBounds,\n SelectionGroup,\n PartSummary,\n} from \"./wasm\";\nimport { BinaryReader, DECODERS, PAINTERS } from \"./binaryDisplayListCommands\";\n\nfunction readPages(r: BinaryReader, numPages: number): PageLayout[] {\n const pages: PageLayout[] = [];\n for (let p = 0; p < numPages; p++) {\n const pageNumber = r.f32();\n const numSystems = r.f32();\n const systemIndices: number[] = [];\n for (let s = 0; s < numSystems; s++) {\n systemIndices.push(r.f32());\n }\n const yOffset = r.f32();\n const height = r.f32();\n pages.push({ pageNumber, systemIndices, yOffset, height });\n }\n return pages;\n}\n\nfunction readElementBboxes(r: BinaryReader, numBboxes: number): ElementBBox[] {\n const bboxes: ElementBBox[] = [];\n for (let i = 0; i < numBboxes; i++) {\n const idLen = r.f32();\n let elementId = \"\";\n for (let j = 0; j < idLen; j++) {\n elementId += String.fromCodePoint(r.f32());\n }\n const bbox: BoundingBox = {\n x: r.f32(),\n y: r.f32(),\n width: r.f32(),\n height: r.f32(),\n };\n bboxes.push({ elementId, bbox });\n }\n return bboxes;\n}\n\nfunction readSlurGeometries(r: BinaryReader, n: number): SlurGeometry[] {\n const out: SlurGeometry[] = [];\n for (let i = 0; i < n; i++) {\n const idLen = r.f32();\n let elementId = \"\";\n for (let j = 0; j < idLen; j++) {\n elementId += String.fromCodePoint(r.f32());\n }\n out.push({\n elementId,\n p0x: r.f32(),\n p0y: r.f32(),\n p1x: r.f32(),\n p1y: r.f32(),\n p2x: r.f32(),\n p2y: r.f32(),\n p3x: r.f32(),\n p3y: r.f32(),\n thickness: r.f32(),\n curveDir: r.f32(),\n sp: r.f32(),\n });\n }\n return out;\n}\n\nfunction readCodepointString(r: BinaryReader, len: number): string {\n let value = \"\";\n for (let i = 0; i < len; i++) {\n value += String.fromCodePoint(r.f32());\n }\n return value;\n}\n\nfunction readMeasureBounds(r: BinaryReader, n: number): MeasureBounds[] {\n const out: MeasureBounds[] = [];\n for (let i = 0; i < n; i++) {\n const idLen = r.f32();\n const measureId = idLen >= 0 ? readCodepointString(r, idLen) : undefined;\n const index = r.f32();\n const partIndex = r.f32();\n const staffIndex = r.f32();\n const systemIndex = r.f32();\n const x = r.f32();\n const width = r.f32();\n const y = r.f32();\n const height = r.f32();\n const prefixWidth = r.f32();\n const totalBeats = r.f32();\n const beatAnchorCount = r.f32();\n const beatAnchors: [number, number][] = [];\n for (let j = 0; j < beatAnchorCount; j++) {\n beatAnchors.push([r.f32(), r.f32()]);\n }\n\n const ghostStaff = r.f32() !== 0;\n const isHidden = r.f32() !== 0;\n const hasMusicHidden = r.f32() !== 0;\n const isExpansion = r.f32() !== 0;\n out.push({\n index,\n ...(measureId !== undefined ? { measureId } : {}),\n partIndex,\n partId: `#${partIndex}`,\n staffIndex,\n systemIndex,\n x,\n width,\n y,\n height,\n prefixWidth,\n totalBeats,\n beatAnchors,\n ...(ghostStaff ? { ghostStaff } : {}),\n ...(isHidden ? { isHidden } : {}),\n ...(hasMusicHidden ? { hasMusicHidden } : {}),\n ...(isExpansion ? { isExpansion } : {}),\n });\n }\n return out;\n}\n\nfunction readSelectionGroups(r: BinaryReader, count: number): SelectionGroup[] {\n const groups: SelectionGroup[] = [];\n for (let index = 0; index < count; index++) {\n const elementId = readCodepointString(r, r.f32());\n const memberCount = r.f32();\n const memberIds: string[] = [];\n for (let member = 0; member < memberCount; member++) {\n memberIds.push(readCodepointString(r, r.f32()));\n }\n groups.push({ elementId, memberIds });\n }\n return groups;\n}\n\nfunction readElementIds(r: BinaryReader, numCommands: number, numStrings: number): (string | null)[] | undefined {\n if (numStrings === 0) {\n // Skip per-command indices (all -1.0)\n r.skip(numCommands);\n return undefined;\n }\n const stringTable: string[] = [];\n for (let s = 0; s < numStrings; s++) {\n const charCount = r.f32();\n let str = \"\";\n for (let i = 0; i < charCount; i++) {\n str += String.fromCodePoint(r.f32());\n }\n stringTable.push(str);\n }\n const ids: (string | null)[] = [];\n for (let c = 0; c < numCommands; c++) {\n const idx = r.f32();\n ids.push(idx < 0 ? null : (stringTable[idx] ?? null));\n }\n return ids;\n}\n\n/**\n * Decode a binary Float32Array into a DisplayList object.\n * Use this when you need the structured data (e.g., for testing or JSON fallback).\n */\nexport function decodeBinaryDisplayList(data: Float32Array): DisplayList {\n const r = new BinaryReader(data);\n\n const width = r.f32();\n const height = r.f32();\n const numCommands = r.f32();\n const numPages = r.f32();\n const numStrings = r.f32();\n const numBboxes = r.f32();\n const numSlurGeometries = r.f32();\n\n const pages = readPages(r, numPages);\n const elementBboxes = readElementBboxes(r, numBboxes);\n const slurGeometries = readSlurGeometries(r, numSlurGeometries);\n\n const commands: RenderCommand[] = [];\n for (let c = 0; c < numCommands; c++) {\n const tag = r.f32();\n const decoder = DECODERS[tag];\n if (!decoder) {\n throw new Error(`Unknown binary display list command tag: ${tag} at offset ${r.pos - 1}`);\n }\n commands.push(decoder(r));\n }\n\n const elementIds = readElementIds(r, numCommands, numStrings);\n const measureBounds = r.pos < data.length ? readMeasureBounds(r, r.f32()) : [];\n const selectionGroups = r.pos < data.length ? readSelectionGroups(r, r.f32()) : [];\n const parts: PartSummary[] = [];\n if (r.pos < data.length) {\n const entryCount = r.f32();\n for (let i = 0; i < entryCount; i++) {\n const boundsIndex = r.f32();\n const sourceCount = r.f32();\n const sources: number[] = [];\n for (let j = 0; j < sourceCount; j++) sources.push(r.f32());\n const bounds = measureBounds[boundsIndex];\n if (bounds && sources.length > 0) bounds.sourcePartIndices = sources;\n }\n }\n if (r.pos < data.length) {\n const count = r.f32();\n for (let i = 0; i < count; i++) {\n parts.push({\n index: r.f32(),\n id: readCodepointString(r, r.f32()),\n name: readCodepointString(r, r.f32()),\n });\n }\n const boundsCount = r.f32();\n for (let i = 0; i < boundsCount; i++) {\n const id = readCodepointString(r, r.f32());\n const bounds = measureBounds[i];\n if (bounds) bounds.partId = id;\n }\n }\n\n const result: DisplayList = { commands, width, height };\n if (pages.length > 0) result.pages = pages;\n if (elementIds) result.elementIds = elementIds;\n if (elementBboxes.length > 0) result.elementBboxes = elementBboxes;\n if (slurGeometries.length > 0) result.slurGeometries = slurGeometries;\n if (measureBounds.length > 0) result.measureBounds = measureBounds;\n if (parts.length > 0) result.parts = parts;\n if (selectionGroups.length > 0) result.selectionGroups = selectionGroups;\n return result;\n}\n\nfunction skipPaintHeaderTables(r: BinaryReader, numPages: number, numBboxes: number, numSlurGeometries: number): void {\n // Skip page data\n for (let p = 0; p < numPages; p++) {\n r.skip(1); // page_number\n const numSystems = r.f32();\n r.skip(numSystems); // system_indices\n r.skip(2); // y_offset, height\n }\n // Skip element bbox data\n for (let i = 0; i < numBboxes; i++) {\n const idLen = r.f32();\n r.skip(idLen); // id codepoints\n r.skip(4); // x, y, width, height\n }\n // Skip slur geometry data\n for (let i = 0; i < numSlurGeometries; i++) {\n const idLen = r.f32();\n r.skip(idLen); // id codepoints\n r.skip(11); // p0..p3 (8) + thickness + curveDir + sp\n }\n}\n\n/**\n * Paint a binary display list directly to a Canvas 2D context.\n * This is the fastest path \u2014 no intermediate object allocation.\n */\nexport function paintBinaryDisplayList(ctx: CanvasRenderingContext2D, data: Float32Array): void {\n const r = new BinaryReader(data);\n\n const width = r.f32();\n const height = r.f32();\n const numCommands = r.f32();\n const numPages = r.f32();\n r.skip(1); // num_strings (not needed for painting)\n const numBboxes = r.f32();\n const numSlurGeometries = r.f32();\n\n skipPaintHeaderTables(r, numPages, numBboxes, numSlurGeometries);\n\n ctx.clearRect(0, 0, width, height);\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, width, height);\n\n for (let c = 0; c < numCommands; c++) {\n const tag = r.f32();\n const painter = PAINTERS[tag];\n if (!painter) {\n throw new Error(`Unknown binary command tag: ${tag}`);\n }\n painter(ctx, r);\n }\n}\n\n/**\n * Extract just the width and height from a binary display list header.\n * Useful for canvas sizing without full decode.\n */\nexport function getBinaryDisplayListDimensions(data: Float32Array): {\n width: number;\n height: number;\n} {\n const r = new BinaryReader(data);\n return { width: r.f32(), height: r.f32() };\n}\n\n/**\n * Extract page layout information from a binary display list.\n */\nexport function getBinaryDisplayListPages(data: Float32Array): PageLayout[] {\n const r = new BinaryReader(data);\n r.skip(3); // width, height, numCommands\n const numPages = r.f32();\n r.skip(3); // num_strings, numBboxes, numSlurGeometries\n return readPages(r, numPages);\n}\n\n/**\n * Extract element bounding boxes from a binary display list.\n */\nexport function getBinaryDisplayListBboxes(data: Float32Array): ElementBBox[] {\n const r = new BinaryReader(data);\n r.skip(3); // width, height, numCommands\n const numPages = r.f32();\n r.skip(1); // numStrings\n const numBboxes = r.f32();\n r.skip(1); // numSlurGeometries\n\n // Skip page data\n for (let p = 0; p < numPages; p++) {\n r.skip(1); // page_number\n const numSystems = r.f32();\n r.skip(numSystems); // system_indices\n r.skip(2); // y_offset, height\n }\n\n return readElementBboxes(r, numBboxes);\n}\n", "/**\n * Per-tag binary display list decoders and painters.\n *\n * Split out of `binaryDisplayList.ts` so that the two top-level entry\n * points (`decodeBinaryDisplayList`, `paintBinaryDisplayList`) stay\n * within complexity and length budgets \u2014 see eslint config.\n */\n\nimport type { RenderCommand } from \"./wasm\";\nimport { canvasFontFamily } from \"./textFont\";\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Tag and table constants (must match Rust binary.rs)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst TAG_DRAW_LINE = 1;\nconst TAG_DRAW_RECT = 2;\nconst TAG_DRAW_CIRCLE = 3;\nconst TAG_DRAW_ELLIPSE = 4;\nconst TAG_DRAW_GLYPH = 5;\nconst TAG_DRAW_BEZIER = 6;\nconst TAG_DRAW_QUADRATIC = 7;\nconst TAG_DRAW_FILLED_BEZIER = 8;\nconst TAG_DRAW_POLYGON = 9;\nconst TAG_DRAW_TEXT = 10;\nconst TAG_SET_OPACITY = 11;\nconst TAG_DRAW_STRETCHED_GLYPH = 12;\n\n// Font IDs match the engine's binary protocol (render/binary.rs):\n// 0 = Bravura (music), then text fonts packed as 1 + family*4 + style where\n// family is serif=0/sans-serif=1/monospace=2 and style is\n// (bold) | (italic << 1).\nconst FONT_TABLE = [\n \"Bravura\",\n \"serif\",\n \"serif\",\n \"serif\",\n \"serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"monospace\",\n \"monospace\",\n \"monospace\",\n \"monospace\",\n] as const;\nconst FONT_STYLE_TABLE = [\n \"\",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n] as const;\nconst FONT_DECODE_TABLE = [\n \"Bravura\",\n \"serif\",\n \"serif bold\",\n \"serif italic\",\n \"serif bold italic\",\n \"sans-serif\",\n \"sans-serif bold\",\n \"sans-serif italic\",\n \"sans-serif bold italic\",\n \"monospace\",\n \"monospace bold\",\n \"monospace italic\",\n \"monospace bold italic\",\n] as const;\n\nconst ALIGN_TABLE: readonly CanvasTextAlign[] = [\"left\", \"center\", \"right\"];\nconst BASELINE_TABLE: readonly CanvasTextBaseline[] = [\"top\", \"middle\", \"bottom\", \"alphabetic\"];\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// BinaryReader \u2014 fast typed-array reader for the binary buffer.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class BinaryReader {\n private readonly f32Data: Float32Array;\n private readonly u32Data: Uint32Array;\n pos: number = 0;\n\n constructor(data: Float32Array) {\n this.f32Data = data;\n this.u32Data = new Uint32Array(data.buffer, data.byteOffset, data.length);\n }\n\n f32(): number {\n const val = this.f32Data[this.pos] ?? 0;\n this.pos++;\n return val;\n }\n\n u32(): number {\n const val = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return val;\n }\n\n color(): string {\n const rgb = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return \"#\" + (rgb & 0xffffff).toString(16).padStart(6, \"0\").toUpperCase();\n }\n\n colorFast(): string {\n const rgb = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return \"#\" + (rgb & 0xffffff).toString(16).padStart(6, \"0\");\n }\n\n skip(n: number): void {\n this.pos += n;\n }\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Per-tag DECODERS (structured RenderCommand emit)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype Decoder = (r: BinaryReader) => RenderCommand;\n\nfunction decodeDrawLine(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawLine\",\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawRect(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawRect\",\n x: r.f32(),\n y: r.f32(),\n w: r.f32(),\n h: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawCircle(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawCircle\",\n cx: r.f32(),\n cy: r.f32(),\n r: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawEllipse(r: BinaryReader): RenderCommand {\n const cx = r.f32(),\n cy = r.f32();\n const rx = r.f32(),\n ry = r.f32();\n const angle = r.f32();\n const filled = r.f32() !== 0;\n const color = r.color();\n return { type: \"DrawEllipse\", cx, cy, rx, ry, angle, filled, color };\n}\n\nfunction decodeDrawGlyph(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n const color = r.color();\n const rotation = r.f32();\n return {\n type: \"DrawGlyph\",\n x,\n y,\n codepoint,\n font: FONT_TABLE[fontId] ?? \"serif\",\n size,\n color,\n rotation,\n };\n}\n\nfunction decodeDrawStretchedGlyph(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n const color = r.color();\n const scale_x = r.f32();\n return {\n type: \"DrawStretchedGlyph\",\n x,\n y,\n codepoint,\n font: FONT_TABLE[fontId] ?? \"serif\",\n size,\n color,\n scale_x,\n };\n}\n\nfunction decodeDrawBezier(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawBezier\",\n x1: r.f32(),\n y1: r.f32(),\n cx1: r.f32(),\n cy1: r.f32(),\n cx2: r.f32(),\n cy2: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawQuadratic(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawQuadratic\",\n x1: r.f32(),\n y1: r.f32(),\n cx: r.f32(),\n cy: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawFilledBezier(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawFilledBezier\",\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n ocx1: r.f32(),\n ocy1: r.f32(),\n ocx2: r.f32(),\n ocy2: r.f32(),\n icx1: r.f32(),\n icy1: r.f32(),\n icx2: r.f32(),\n icy2: r.f32(),\n ix1: r.f32(),\n iy1: r.f32(),\n ix2: r.f32(),\n iy2: r.f32(),\n color: r.color(),\n line_style: r.f32(),\n };\n}\n\nfunction decodeDrawPolygon(r: BinaryReader): RenderCommand {\n const nPoints = r.f32();\n const points: [number, number][] = [];\n for (let i = 0; i < nPoints; i++) {\n points.push([r.f32(), r.f32()]);\n }\n return { type: \"DrawPolygon\", points, color: r.color() };\n}\n\nfunction decodeDrawText(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const size = r.f32();\n const color = r.color();\n const alignIdx = r.f32();\n const baselineIdx = r.f32();\n const fontId = r.f32();\n const textLen = r.f32();\n let text = \"\";\n for (let i = 0; i < textLen; i++) {\n text += String.fromCodePoint(r.f32());\n }\n return {\n type: \"DrawText\",\n x,\n y,\n size,\n color,\n align: (ALIGN_TABLE[alignIdx] ?? \"left\") as \"left\" | \"center\" | \"right\",\n baseline: (BASELINE_TABLE[baselineIdx] ?? \"alphabetic\") as \"top\" | \"middle\" | \"bottom\" | \"alphabetic\",\n font: FONT_DECODE_TABLE[fontId] ?? \"serif\",\n text,\n };\n}\n\nfunction decodeSetOpacity(r: BinaryReader): RenderCommand {\n return { type: \"SetOpacity\", opacity: r.f32() };\n}\n\nexport const DECODERS: Record<number, Decoder> = {\n [TAG_DRAW_LINE]: decodeDrawLine,\n [TAG_DRAW_RECT]: decodeDrawRect,\n [TAG_DRAW_CIRCLE]: decodeDrawCircle,\n [TAG_DRAW_ELLIPSE]: decodeDrawEllipse,\n [TAG_DRAW_GLYPH]: decodeDrawGlyph,\n [TAG_DRAW_STRETCHED_GLYPH]: decodeDrawStretchedGlyph,\n [TAG_DRAW_BEZIER]: decodeDrawBezier,\n [TAG_DRAW_QUADRATIC]: decodeDrawQuadratic,\n [TAG_DRAW_FILLED_BEZIER]: decodeDrawFilledBezier,\n [TAG_DRAW_POLYGON]: decodeDrawPolygon,\n [TAG_DRAW_TEXT]: decodeDrawText,\n [TAG_SET_OPACITY]: decodeSetOpacity,\n};\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Per-tag PAINTERS (direct canvas paint)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype Painter = (ctx: CanvasRenderingContext2D, r: BinaryReader) => void;\n\nfunction paintDrawLine(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32(),\n x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n}\n\nfunction paintDrawRect(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32(),\n w = r.f32(),\n h = r.f32();\n ctx.fillStyle = r.colorFast();\n ctx.fillRect(x, y, w, h);\n}\n\nfunction paintDrawCircle(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const cx = r.f32(),\n cy = r.f32(),\n radius = r.f32();\n ctx.fillStyle = r.colorFast();\n ctx.beginPath();\n ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n ctx.fill();\n}\n\nfunction paintDrawEllipse(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const cx = r.f32(),\n cy = r.f32(),\n rx = r.f32(),\n ry = r.f32();\n const angle = r.f32();\n const filled = r.f32() !== 0;\n const color = r.colorFast();\n ctx.save();\n ctx.translate(cx, cy);\n ctx.rotate(angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);\n if (filled) {\n ctx.fillStyle = color;\n ctx.fill();\n } else {\n ctx.strokeStyle = color;\n ctx.lineWidth = Math.max(rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n}\n\nfunction paintDrawGlyph(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const rotation = r.f32();\n ctx.font = `${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n if (rotation !== 0) {\n ctx.save();\n ctx.translate(x, y);\n ctx.rotate(rotation);\n ctx.fillText(String.fromCodePoint(codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(codepoint), x, y);\n }\n}\n\nfunction paintDrawStretchedGlyph(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const scaleX = r.f32();\n ctx.font = `${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(x, y);\n ctx.scale(scaleX, 1);\n ctx.fillText(String.fromCodePoint(codepoint), 0, 0);\n ctx.restore();\n}\n\nfunction paintDrawBezier(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32();\n const cx1 = r.f32(),\n cy1 = r.f32();\n const cx2 = r.f32(),\n cy2 = r.f32();\n const x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);\n ctx.stroke();\n}\n\nfunction paintDrawQuadratic(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32();\n const cx = r.f32(),\n cy = r.f32();\n const x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.quadraticCurveTo(cx, cy, x2, y2);\n ctx.stroke();\n}\n\ninterface FilledBezierGeom {\n x1: number;\n y1: number;\n x2: number;\n y2: number;\n ocx1: number;\n ocy1: number;\n ocx2: number;\n ocy2: number;\n icx1: number;\n icy1: number;\n icx2: number;\n icy2: number;\n ix1: number;\n iy1: number;\n ix2: number;\n iy2: number;\n}\n\nfunction paintFilledBezierSolid(ctx: CanvasRenderingContext2D, g: FilledBezierGeom, fillColor: string): void {\n // Solid: filled crescent shape with finite-width rounded tips.\n // Cap is a quadratic curve bulging outward along the local tangent.\n const cap1tx = g.x1 - g.ocx1;\n const cap1ty = g.y1 - g.ocy1;\n const cap1tl = Math.hypot(cap1tx, cap1ty) || 1;\n const cap1w = Math.hypot(g.x1 - g.ix1, g.y1 - g.iy1);\n const cap1ext = cap1w * 0.55;\n const cap1cx = (g.x1 + g.ix1) / 2 + (cap1tx / cap1tl) * cap1ext;\n const cap1cy = (g.y1 + g.iy1) / 2 + (cap1ty / cap1tl) * cap1ext;\n\n const cap2tx = g.x2 - g.ocx2;\n const cap2ty = g.y2 - g.ocy2;\n const cap2tl = Math.hypot(cap2tx, cap2ty) || 1;\n const cap2w = Math.hypot(g.x2 - g.ix2, g.y2 - g.iy2);\n const cap2ext = cap2w * 0.55;\n const cap2cx = (g.x2 + g.ix2) / 2 + (cap2tx / cap2tl) * cap2ext;\n const cap2cy = (g.y2 + g.iy2) / 2 + (cap2ty / cap2tl) * cap2ext;\n\n ctx.fillStyle = fillColor;\n ctx.beginPath();\n ctx.moveTo(g.x1, g.y1);\n ctx.bezierCurveTo(g.ocx1, g.ocy1, g.ocx2, g.ocy2, g.x2, g.y2);\n ctx.quadraticCurveTo(cap2cx, cap2cy, g.ix2, g.iy2);\n ctx.bezierCurveTo(g.icx2, g.icy2, g.icx1, g.icy1, g.ix1, g.iy1);\n ctx.quadraticCurveTo(cap1cx, cap1cy, g.x1, g.y1);\n ctx.closePath();\n ctx.fill();\n}\n\nfunction paintFilledBezierStroked(\n ctx: CanvasRenderingContext2D,\n g: FilledBezierGeom,\n fillColor: string,\n lineStyle: number,\n): void {\n // Dashed/dotted: stroke through the midline of outer+inner curves\n // standard engraving practice renders dashed slurs as dashed bezier strokes.\n const mcx1 = (g.ocx1 + g.icx1) / 2;\n const mcy1 = (g.ocy1 + g.icy1) / 2;\n const mcx2 = (g.ocx2 + g.icx2) / 2;\n const mcy2 = (g.ocy2 + g.icy2) / 2;\n const thickness = Math.hypot(g.ocx1 - g.icx1, g.ocy1 - g.icy1);\n const strokeWidth = Math.max(thickness * 0.5, 1);\n ctx.strokeStyle = fillColor;\n ctx.lineWidth = strokeWidth;\n if (lineStyle === 1) {\n // Dashed\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (lineStyle === 2) {\n // Dotted\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n // Unknown style \u2014 fallback to solid stroke\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(g.x1, g.y1);\n ctx.bezierCurveTo(mcx1, mcy1, mcx2, mcy2, g.x2, g.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\nfunction paintDrawFilledBezier(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const g: FilledBezierGeom = {\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n ocx1: r.f32(),\n ocy1: r.f32(),\n ocx2: r.f32(),\n ocy2: r.f32(),\n icx1: r.f32(),\n icy1: r.f32(),\n icx2: r.f32(),\n icy2: r.f32(),\n ix1: r.f32(),\n iy1: r.f32(),\n ix2: r.f32(),\n iy2: r.f32(),\n };\n const fillColor = r.colorFast();\n const lineStyle = r.f32();\n if (lineStyle === 0) {\n paintFilledBezierSolid(ctx, g, fillColor);\n } else {\n paintFilledBezierStroked(ctx, g, fillColor, lineStyle);\n }\n}\n\nfunction paintDrawPolygon(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const nPoints = r.f32();\n if (nPoints >= 3) {\n ctx.beginPath();\n ctx.moveTo(r.f32(), r.f32());\n for (let i = 1; i < nPoints; i++) {\n ctx.lineTo(r.f32(), r.f32());\n }\n ctx.fillStyle = r.colorFast();\n ctx.closePath();\n ctx.fill();\n } else {\n r.skip(nPoints * 2 + 1);\n }\n}\n\nfunction paintDrawText(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const alignIdx = r.f32();\n const baselineIdx = r.f32();\n const fontId = r.f32();\n const textLen = r.f32();\n let text = \"\";\n for (let i = 0; i < textLen; i++) {\n text += String.fromCodePoint(r.f32());\n }\n ctx.font = `${FONT_STYLE_TABLE[fontId] ?? \"\"}${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = ALIGN_TABLE[alignIdx] ?? \"left\";\n ctx.textBaseline = BASELINE_TABLE[baselineIdx] ?? \"alphabetic\";\n ctx.fillText(text, x, y);\n}\n\nfunction paintSetOpacity(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n ctx.globalAlpha = r.f32();\n}\n\nexport const PAINTERS: Record<number, Painter> = {\n [TAG_DRAW_LINE]: paintDrawLine,\n [TAG_DRAW_RECT]: paintDrawRect,\n [TAG_DRAW_CIRCLE]: paintDrawCircle,\n [TAG_DRAW_ELLIPSE]: paintDrawEllipse,\n [TAG_DRAW_GLYPH]: paintDrawGlyph,\n [TAG_DRAW_STRETCHED_GLYPH]: paintDrawStretchedGlyph,\n [TAG_DRAW_BEZIER]: paintDrawBezier,\n [TAG_DRAW_QUADRATIC]: paintDrawQuadratic,\n [TAG_DRAW_FILLED_BEZIER]: paintDrawFilledBezier,\n [TAG_DRAW_POLYGON]: paintDrawPolygon,\n [TAG_DRAW_TEXT]: paintDrawText,\n [TAG_SET_OPACITY]: paintSetOpacity,\n};\n", "import type { RenderCommand } from \"./wasm\";\n\nexport interface RenderBounds {\n x: number;\n y: number;\n x2: number;\n y2: number;\n}\n\n/** Conservative command bounds shared by direct-paint and retained-layer culling. */\nexport function renderCommandBounds(cmd: RenderCommand): RenderBounds | null {\n switch (cmd.type) {\n case \"DrawEllipse\":\n return { x: cmd.cx - cmd.rx, y: cmd.cy - cmd.ry, x2: cmd.cx + cmd.rx, y2: cmd.cy + cmd.ry };\n case \"DrawLine\":\n return {\n x: Math.min(cmd.x1, cmd.x2) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2) + cmd.width,\n };\n case \"DrawRect\":\n return { x: cmd.x, y: cmd.y, x2: cmd.x + cmd.w, y2: cmd.y + cmd.h };\n case \"DrawCircle\":\n return { x: cmd.cx - cmd.r, y: cmd.cy - cmd.r, x2: cmd.cx + cmd.r, y2: cmd.cy + cmd.r };\n case \"DrawGlyph\":\n return { x: cmd.x - cmd.size, y: cmd.y - cmd.size, x2: cmd.x + cmd.size * 2, y2: cmd.y + cmd.size };\n case \"DrawStretchedGlyph\":\n // A stretched glyph is only ever narrowed, so the proportional box still\n // contains it; its height is the one dimension `size` speaks for.\n return { x: cmd.x - cmd.size, y: cmd.y - cmd.size, x2: cmd.x + cmd.size * 2, y2: cmd.y + cmd.size };\n case \"DrawText\": {\n const charCount = cmd.text?.length ?? 1;\n const estimatedWidth = charCount * cmd.size * 0.6;\n const left =\n cmd.align === \"right\" ? cmd.x - estimatedWidth : cmd.align === \"center\" ? cmd.x - estimatedWidth / 2 : cmd.x;\n return { x: left, y: cmd.y - cmd.size, x2: left + estimatedWidth, y2: cmd.y + cmd.size * 0.3 };\n }\n case \"DrawBezier\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.cx1, cmd.cx2) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2, cmd.cx1, cmd.cx2) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2) + cmd.width,\n };\n case \"DrawQuadratic\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.cx) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2, cmd.cy) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2, cmd.cx) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2, cmd.cy) + cmd.width,\n };\n case \"DrawFilledBezier\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.ocx1, cmd.ocx2, cmd.icx1, cmd.icx2),\n y: Math.min(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n x2: Math.max(cmd.x1, cmd.x2, cmd.ocx1, cmd.ocx2, cmd.icx1, cmd.icx2),\n y2: Math.max(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n };\n case \"DrawPolygon\": {\n if (cmd.points.length === 0) return null;\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const [px, py] of cmd.points) {\n minX = Math.min(minX, px);\n minY = Math.min(minY, py);\n maxX = Math.max(maxX, px);\n maxY = Math.max(maxY, py);\n }\n return { x: minX, y: minY, x2: maxX, y2: maxY };\n }\n case \"SetOpacity\":\n return null;\n }\n}\n", "/**\n * PageCache \u2014 OffscreenCanvas page cache with 5-page sliding window.\n *\n * Each page is rendered once to its own OffscreenCanvas. The visible canvas\n * composites cached pages via drawImage() for 60fps scrolling.\n *\n * Ref: docs/spec/performance-architecture.md \u00A73.1\n */\n\nimport type { DisplayList, PageLayout, RenderCommand } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { traceFilledBezier } from \"./displayListPainter\";\nimport { canvasTextFont } from \"./textFont\";\n\n/** Sliding window size: current page \u00B1 2. */\nconst WINDOW_SIZE = 5;\nconst WINDOW_BEFORE = 2;\n\ninterface CachedPage {\n pageIndex: number;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n}\n\n/**\n * Split a DisplayList into per-page command arrays using the pages metadata.\n * Commands are assigned to a page based on their y-coordinate falling within\n * the page's yOffset..yOffset+height range.\n */\nexport function splitCommandsByPage(displayList: DisplayList): { commands: RenderCommand[]; layout: PageLayout }[] {\n const pages = displayList.pages;\n if (!pages || pages.length === 0) {\n // No page info \u2014 treat entire displayList as one page\n return [\n {\n commands: displayList.commands,\n layout: {\n pageNumber: 1,\n systemIndices: [],\n yOffset: 0,\n height: displayList.height,\n },\n },\n ];\n }\n\n const result: { commands: RenderCommand[]; layout: PageLayout }[] = pages.map((p) => ({ commands: [], layout: p }));\n\n for (const cmd of displayList.commands) {\n const [cmdMinY, cmdMaxY] = getCommandYRange(cmd);\n // Assign to every page whose vertical extent overlaps the command's\n // y-range. This is what makes cross-page geometry (slurs that bridge a\n // page break, brackets that span systems, long beam lines, etc.) render\n // correctly: each page's OffscreenCanvas paints its slice, and the\n // out-of-bounds remainder is silently clipped by Canvas 2D.\n let assigned = false;\n for (let i = 0; i < pages.length; i++) {\n const page = pages[i];\n if (!page) continue;\n const pageTop = page.yOffset;\n const pageBottom = page.yOffset + page.height;\n if (cmdMaxY >= pageTop && cmdMinY < pageBottom) {\n const entry = result[i];\n if (entry) entry.commands.push(cmd);\n assigned = true;\n }\n }\n // If no page matched (e.g. global background), assign to first visible page\n if (!assigned && pages.length > 0) {\n const first = result[0];\n if (first) first.commands.push(cmd);\n }\n }\n\n return result;\n}\n\n/**\n * Extract the [minY, maxY] vertical extent of a render command. Used to\n * decide which page(s) a command belongs to so cross-page geometry (slurs,\n * brackets, long beams) renders on every page it crosses.\n */\nfunction getCommandYRange(cmd: RenderCommand): [number, number] {\n switch (cmd.type) {\n case \"DrawLine\":\n return [Math.min(cmd.y1, cmd.y2), Math.max(cmd.y1, cmd.y2)];\n case \"DrawRect\":\n return [cmd.y, cmd.y + cmd.h];\n case \"DrawCircle\":\n return [cmd.cy - cmd.r, cmd.cy + cmd.r];\n case \"DrawEllipse\":\n return [cmd.cy - cmd.ry, cmd.cy + cmd.ry];\n case \"DrawText\":\n return [cmd.y, cmd.y];\n case \"DrawGlyph\":\n return [cmd.y, cmd.y];\n case \"DrawStretchedGlyph\":\n // A brace hangs from its baseline, so the span it covers is the `size`\n // above `y` \u2014 a page it crosses has to keep it.\n return [cmd.y - cmd.size, cmd.y];\n case \"DrawBezier\":\n return [Math.min(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2), Math.max(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2)];\n case \"DrawQuadratic\":\n return [Math.min(cmd.y1, cmd.y2, cmd.cy), Math.max(cmd.y1, cmd.y2, cmd.cy)];\n case \"DrawFilledBezier\":\n return [\n Math.min(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n Math.max(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n ];\n case \"DrawPolygon\": {\n if (cmd.points.length === 0) return [0, 0];\n const ys = cmd.points.map((p) => p[1]);\n return [Math.min(...ys), Math.max(...ys)];\n }\n case \"SetOpacity\":\n return [0, 0];\n }\n}\n\nexport class PageCache {\n private cache: Map<number, CachedPage> = new Map();\n private totalPages = 0;\n private displayList: DisplayList | null = null;\n private pageCommands: { commands: RenderCommand[]; layout: PageLayout }[] = [];\n private glyphAtlas: GlyphAtlas | null = null;\n\n /** Optional painter function override (for testing). */\n private paintFn:\n ((ctx: OffscreenCanvasRenderingContext2D, commands: RenderCommand[], atlas: GlyphAtlas | null) => void) | null =\n null;\n\n constructor(glyphAtlas?: GlyphAtlas) {\n if (glyphAtlas) {\n this.glyphAtlas = glyphAtlas;\n }\n }\n\n /** Set the glyph atlas for optimized rendering. */\n setGlyphAtlas(atlas: GlyphAtlas): void {\n this.glyphAtlas = atlas;\n }\n\n /** Set a custom paint function (for testing). */\n setPaintFn(\n fn: (ctx: OffscreenCanvasRenderingContext2D, commands: RenderCommand[], atlas: GlyphAtlas | null) => void,\n ): void {\n this.paintFn = fn;\n }\n\n /**\n * Load a display list and split it by pages.\n * Invalidates the entire cache.\n */\n setDisplayList(displayList: DisplayList): void {\n this.displayList = displayList;\n this.pageCommands = splitCommandsByPage(displayList);\n this.totalPages = this.pageCommands.length;\n this.cache.clear();\n }\n\n /** Number of pages in the current display list. */\n get pageCount(): number {\n return this.totalPages;\n }\n\n /**\n * Ensure the 5-page window around currentPage is cached.\n * Evicts pages outside the window.\n */\n ensureWindow(currentPage: number): void {\n if (!this.displayList) return;\n\n const windowStart = Math.max(0, currentPage - WINDOW_BEFORE);\n const windowEnd = Math.min(this.totalPages - 1, windowStart + WINDOW_SIZE - 1);\n\n // Evict pages outside window\n for (const [pageIndex] of this.cache) {\n if (pageIndex < windowStart || pageIndex > windowEnd) {\n this.cache.delete(pageIndex);\n }\n }\n\n // Render pages in the window\n for (let i = windowStart; i <= windowEnd; i++) {\n if (!this.cache.has(i)) {\n this.renderPage(i);\n }\n }\n }\n\n /**\n * Render a single page to its own OffscreenCanvas.\n */\n private renderPage(pageIndex: number): void {\n if (pageIndex < 0 || pageIndex >= this.totalPages || !this.displayList) return;\n\n const pageData = this.pageCommands[pageIndex];\n if (!pageData) return;\n const width = this.displayList.width;\n const height = pageData.layout.height;\n\n const canvas = new OffscreenCanvas(Math.ceil(width), Math.ceil(height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // White background\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, width, height);\n\n // Translate so page content renders at y=0\n const yOff = pageData.layout.yOffset;\n ctx.translate(0, -yOff);\n\n if (this.paintFn) {\n this.paintFn(ctx, pageData.commands, this.glyphAtlas);\n } else {\n // Use the imported paint function\n paintCommandsWithAtlas(ctx, pageData.commands, this.glyphAtlas);\n }\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n\n this.cache.set(pageIndex, { pageIndex, canvas, width, height });\n }\n\n /**\n * Composite cached pages onto the visible canvas.\n */\n compositeToCanvas(\n ctx: CanvasRenderingContext2D,\n canvasWidth: number,\n canvasHeight: number,\n scrollY: number = 0,\n ): void {\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, canvasWidth, canvasHeight);\n\n for (const [, page] of this.cache) {\n const pageData = this.pageCommands[page.pageIndex];\n if (!pageData) continue;\n\n const pageTop = pageData.layout.yOffset - scrollY;\n const pageBottom = pageTop + page.height;\n\n // Skip if not visible\n if (pageBottom < 0 || pageTop > canvasHeight) continue;\n\n ctx.drawImage(page.canvas, 0, pageTop);\n }\n }\n\n /** Check if a specific page is cached. */\n isPageCached(pageIndex: number): boolean {\n return this.cache.has(pageIndex);\n }\n\n /** Get the number of currently cached pages. */\n get cachedPageCount(): number {\n return this.cache.size;\n }\n\n /** Invalidate the entire cache (e.g. on zoom change). */\n invalidate(): void {\n this.cache.clear();\n }\n}\n\n/**\n * Paint a list of commands using the glyph atlas when possible,\n * falling back to direct rendering.\n */\nfunction paintCommandsWithAtlas(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n commands: RenderCommand[],\n atlas: GlyphAtlas | null,\n): void {\n for (const cmd of commands) {\n if (cmd.type === \"DrawGlyph\" && atlas?.isBuilt && cmd.rotation === 0) {\n const drawn = atlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintSingleCommand(ctx, cmd);\n }\n}\n\ntype Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;\ntype FilledBezierCmd = Extract<RenderCommand, { type: \"DrawFilledBezier\" }>;\n\nfunction paintFilledBezierCached(ctx: Ctx2D, cmd: FilledBezierCmd): void {\n if (cmd.line_style === 0 || cmd.line_style === undefined) {\n ctx.fillStyle = cmd.color;\n traceFilledBezier(ctx, cmd);\n ctx.fill();\n return;\n }\n const mcx1 = (cmd.ocx1 + cmd.icx1) / 2;\n const mcy1 = (cmd.ocy1 + cmd.icy1) / 2;\n const mcx2 = (cmd.ocx2 + cmd.icx2) / 2;\n const mcy2 = (cmd.ocy2 + cmd.icy2) / 2;\n const thickness = Math.hypot(cmd.ocx1 - cmd.icx1, cmd.ocy1 - cmd.icy1);\n const strokeWidth = Math.max(thickness * 0.5, 1);\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = strokeWidth;\n if (cmd.line_style === 1) {\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (cmd.line_style === 2) {\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(mcx1, mcy1, mcx2, mcy2, cmd.x2, cmd.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\n/**\n * Paint a single render command (fallback path, same as displayListPainter).\n */\nfunction paintSingleCommand(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n cmd: RenderCommand,\n): void {\n switch (cmd.type) {\n case \"DrawLine\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.lineTo(cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawEllipse\":\n ctx.save();\n ctx.translate(cmd.cx, cmd.cy);\n ctx.rotate(cmd.angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, cmd.rx, cmd.ry, 0, 0, Math.PI * 2);\n if (cmd.filled) {\n ctx.fillStyle = cmd.color;\n ctx.fill();\n } else {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = Math.max(cmd.rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n break;\n\n case \"DrawRect\":\n ctx.fillStyle = cmd.color;\n ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);\n break;\n\n case \"DrawCircle\":\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n ctx.arc(cmd.cx, cmd.cy, cmd.r, 0, Math.PI * 2);\n ctx.fill();\n break;\n\n case \"DrawText\":\n ctx.fillStyle = cmd.color;\n ctx.font = canvasTextFont(cmd.font, cmd.size);\n ctx.textAlign = cmd.align;\n ctx.textBaseline = cmd.baseline;\n ctx.fillText(cmd.text, cmd.x, cmd.y);\n break;\n\n case \"DrawGlyph\":\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n if (cmd.rotation !== 0) {\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.rotate(cmd.rotation);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(cmd.codepoint), cmd.x, cmd.y);\n }\n break;\n\n case \"DrawStretchedGlyph\":\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.scale(cmd.scale_x, 1);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n break;\n\n case \"DrawBezier\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.cx1, cmd.cy1, cmd.cx2, cmd.cy2, cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawQuadratic\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.quadraticCurveTo(cmd.cx, cmd.cy, cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawFilledBezier\":\n paintFilledBezierCached(ctx, cmd);\n break;\n\n case \"DrawPolygon\": {\n const pts = cmd.points;\n if (pts.length >= 3) {\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n const first = pts[0];\n if (first) ctx.moveTo(first[0], first[1]);\n for (let i = 1; i < pts.length; i++) {\n const pt = pts[i];\n if (pt) ctx.lineTo(pt[0], pt[1]);\n }\n ctx.closePath();\n ctx.fill();\n }\n break;\n }\n }\n}\n", "/**\n * WASM Engine wrapper \u2014 loads and initializes the Viritura WASM module.\n *\n * This wraps the wasm-bindgen generated JS glue code and provides\n * a clean TypeScript API for the editor to use.\n */\n\nimport { decodeBinaryDisplayList } from \"./binaryDisplayList\";\n\n// Type re-exports \u2014 definitions live in wasmTypes.ts.\nexport * from \"./wasmTypes\";\nimport type {\n BoundingBox,\n DisplayList,\n NotePreviewInput,\n RenderCommand,\n ScoreInfo,\n SlurPreview,\n SlurPreviewInput,\n} from \"./wasmTypes\";\n\ndeclare const __VIRITURA_WASM_ASSET_HASH__: string | undefined;\n\n// Module-level state\nlet wasmModule: {\n compute_layout: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_layout_binary: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_full_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_full_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_mnx_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => string;\n compute_mnx_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => Float32Array;\n get_score_info: (mnx_json: string) => string;\n engine_version: () => string;\n compute_slur_preview: (preview_json: string) => string;\n compute_note_preview?: (preview_json: string) => string;\n export_svg: (\n display_list_json: string,\n bravura_data: Uint8Array,\n text_font_data: Uint8Array,\n spatium_mm: number,\n sp_pixels: number,\n page_width_mm: number,\n page_height_mm: number,\n ) => string;\n set_emit_layout_debug?: (enabled: boolean) => void;\n get_emit_layout_debug?: () => boolean;\n set_wasm_timing?: (enabled: boolean) => void;\n} | null = null;\n\nlet initPromise: Promise<void> | null = null;\nlet lastInitError: unknown = null;\n\n/** The error from the most recent failed {@link initWasm} attempt, or null. */\nexport function getWasmInitError(): unknown {\n return lastInitError;\n}\n\nimport { resolveBasePath } from \"./basePath\";\n\n/**\n * Initialize the WASM engine. Must be called before any other function.\n * Safe to call multiple times \u2014 only initializes once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n if (initPromise) return initPromise;\n\n const attempt = (async () => {\n try {\n const basePath = resolveBasePath();\n const assetHash =\n typeof __VIRITURA_WASM_ASSET_HASH__ === \"string\" && __VIRITURA_WASM_ASSET_HASH__.length > 0\n ? `.${__VIRITURA_WASM_ASSET_HASH__}`\n : \"\";\n\n // Step 1: Fetch the WASM binary\n const wasmResponse = await fetch(`${basePath}wasm/viritura_wasm_bg${assetHash}.wasm`);\n if (!wasmResponse.ok) {\n throw new Error(`Failed to fetch WASM binary: ${wasmResponse.status}`);\n }\n const wasmBytes = await wasmResponse.arrayBuffer();\n\n // Step 2: Import the same-origin generated glue directly. Avoiding a\n // blob: module lets production enforce `script-src 'self'`.\n const glue = await import(\n /* @vite-ignore */ /* webpackIgnore: true */ `${basePath}wasm/viritura_wasm${assetHash}.js`\n );\n\n // Step 3: Initialize with the binary bytes (avoids a second WASM fetch).\n const wasmModule_ = new WebAssembly.Module(wasmBytes);\n glue.initSync({ module: wasmModule_ });\n\n wasmModule = glue;\n lastInitError = null;\n _flushPendingDebugFlag();\n console.log(`Viritura WASM engine v${glue.engine_version()} loaded`);\n } catch (e) {\n lastInitError = e;\n console.warn(\"WASM engine failed to load:\", e);\n if (e instanceof Error) {\n console.warn(\"Error details:\", e.message);\n }\n wasmModule = null;\n }\n })();\n initPromise = attempt;\n try {\n await attempt;\n } finally {\n // A transient fetch/CSP/cache failure must not poison initialization for\n // the lifetime of this realm. Successful initialization keeps the resolved\n // promise as the idempotent fast path; failure clears it so a later caller\n // can retry.\n if (!wasmModule && initPromise === attempt) initPromise = null;\n }\n}\n\n/**\n * Check if the WASM engine is loaded and ready.\n */\nexport function isWasmReady(): boolean {\n return wasmModule !== null;\n}\n\n/** Semantic version reported by the loaded WASM engine, or null before init. */\nexport function getEngineVersion(): string | null {\n return wasmModule ? wasmModule.engine_version() : null;\n}\n\nexport function computeSlurPreview(input: SlurPreviewInput): SlurPreview {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n return JSON.parse(wasmModule.compute_slur_preview(JSON.stringify(input))) as SlurPreview;\n}\n\n/** Older/uninitialized WASM can still show the cursor, but cannot engrave a ghost. */\nexport function computeNotePreview(input: NotePreviewInput): RenderCommand[] {\n if (!wasmModule?.compute_note_preview) return [];\n return JSON.parse(wasmModule.compute_note_preview(JSON.stringify(input))) as RenderCommand[];\n}\n\n/**\n * Compute layout using the WASM engine.\n * Returns a DisplayList of render commands.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\n/**\n * Map snake_case keys from Rust JSON to camelCase TypeScript DisplayList.\n * Handles: element_ids \u2192 elementIds, element_bboxes \u2192 elementBboxes\n */\nfunction normalizeDisplayList(raw: Record<string, unknown>): DisplayList {\n const dl = raw as unknown as DisplayList;\n // Remap snake_case \u2192 camelCase if needed\n if (!dl.elementIds && (raw as Record<string, unknown>)[\"element_ids\"]) {\n dl.elementIds = (raw as Record<string, unknown>)[\"element_ids\"] as (string | null)[];\n }\n if (!dl.elementBboxes && (raw as Record<string, unknown>)[\"element_bboxes\"]) {\n const rawBboxes = (raw as Record<string, unknown>)[\"element_bboxes\"] as Record<string, unknown>[];\n dl.elementBboxes = rawBboxes.map((b) => ({\n elementId: (b[\"elementId\"] ?? b[\"element_id\"]) as string,\n bbox: (b[\"bbox\"] ?? b) as BoundingBox,\n }));\n }\n if (!dl.selectionGroups && (raw as Record<string, unknown>)[\"selection_groups\"]) {\n const rawGroups = (raw as Record<string, unknown>)[\"selection_groups\"] as Record<string, unknown>[];\n dl.selectionGroups = rawGroups.map((group) => ({\n elementId: (group[\"elementId\"] ?? group[\"element_id\"]) as string,\n memberIds: (group[\"memberIds\"] ?? group[\"member_ids\"]) as string[],\n }));\n }\n // Normalize measure_bounds \u2192 measureBounds\n if (!dl.measureBounds && (raw as Record<string, unknown>)[\"measure_bounds\"]) {\n const rawBounds = (raw as Record<string, unknown>)[\"measure_bounds\"] as Record<string, unknown>[];\n dl.measureBounds = rawBounds.map((b) => ({\n index: b[\"index\"] as number,\n measureId: (b[\"measureId\"] ?? b[\"measure_id\"]) as string | undefined,\n partIndex: (b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n partId: (b[\"partId\"] ?? b[\"part_id\"]) as string,\n sourcePartIndices: (b[\"sourcePartIndices\"] ?? b[\"source_part_indices\"]) as number[] | undefined,\n staffIndex: (b[\"staffIndex\"] ?? b[\"staff_index\"] ?? b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n systemIndex: (b[\"systemIndex\"] ?? b[\"system_index\"]) as number | undefined,\n x: b[\"x\"] as number,\n width: b[\"width\"] as number,\n y: b[\"y\"] as number,\n height: b[\"height\"] as number,\n prefixWidth: (b[\"prefixWidth\"] ?? b[\"prefix_width\"]) as number,\n totalBeats: (b[\"totalBeats\"] ?? b[\"total_beats\"]) as number,\n beatAnchors: (b[\"beatAnchors\"] ?? b[\"beat_anchors\"]) as [number, number][],\n ghostStaff: (b[\"ghostStaff\"] ?? b[\"ghost_staff\"]) as boolean | undefined,\n isHidden: (b[\"isHidden\"] ?? b[\"is_hidden\"]) as boolean | undefined,\n hasMusicHidden: (b[\"hasMusicHidden\"] ?? b[\"has_music_hidden\"]) as boolean | undefined,\n isExpansion: (b[\"isExpansion\"] ?? b[\"is_expansion\"]) as boolean | undefined,\n }));\n }\n // Normalize layout_debug \u2192 layoutDebug\n if (!dl.layoutDebug && (raw as Record<string, unknown>)[\"layout_debug\"]) {\n dl.layoutDebug = (raw as Record<string, unknown>)[\"layout_debug\"] as DisplayList[\"layoutDebug\"];\n }\n // Normalize slur_geometries \u2192 slurGeometries\n if (!dl.slurGeometries && (raw as Record<string, unknown>)[\"slur_geometries\"]) {\n const rawSlurs = (raw as Record<string, unknown>)[\"slur_geometries\"] as Record<string, unknown>[];\n dl.slurGeometries = rawSlurs.map((s) => ({\n elementId: (s[\"elementId\"] ?? s[\"element_id\"]) as string,\n p0x: (s[\"p0x\"] ?? s[\"p0_x\"]) as number,\n p0y: (s[\"p0y\"] ?? s[\"p0_y\"]) as number,\n p1x: (s[\"p1x\"] ?? s[\"p1_x\"]) as number,\n p1y: (s[\"p1y\"] ?? s[\"p1_y\"]) as number,\n p2x: (s[\"p2x\"] ?? s[\"p2_x\"]) as number,\n p2y: (s[\"p2y\"] ?? s[\"p2_y\"]) as number,\n p3x: (s[\"p3x\"] ?? s[\"p3_x\"]) as number,\n p3y: (s[\"p3y\"] ?? s[\"p3_y\"]) as number,\n thickness: s[\"thickness\"] as number,\n curveDir: (s[\"curveDir\"] ?? s[\"curve_dir\"]) as number,\n sp: s[\"sp\"] as number,\n }));\n }\n // Normalize page_turn_warnings \u2192 pageTurnWarnings\n if (!dl.pageTurnWarnings && (raw as Record<string, unknown>)[\"page_turn_warnings\"]) {\n const rawWarnings = (raw as Record<string, unknown>)[\"page_turn_warnings\"] as Record<string, unknown>[];\n dl.pageTurnWarnings = rawWarnings.map((w) => ({\n boundaryMeasure: (w[\"boundaryMeasure\"] ?? w[\"boundary_measure\"]) as number,\n kind: w[\"kind\"] as string,\n turnSeconds: (w[\"turnSeconds\"] ?? w[\"turn_seconds\"]) as number,\n }));\n }\n return dl;\n}\n\nexport function computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_layout(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout for all parts stacked vertically using the WASM engine.\n * Returns a DisplayList of render commands for the full score.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_full_score_layout(mnxJson, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeLayoutBinary(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_layout_binary(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout for all parts using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_full_score_layout_binary(mnxJson, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout using a specific MNX score definition index.\n * Returns a DisplayList of render commands.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_mnx_score_layout(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Binary sibling of {@link computeMnxScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it to the main\n * thread zero-copy instead of structured-cloning a deep DisplayList.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_mnx_score_layout_binary(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n}\n\n/**\n * Get score metadata from MNX JSON.\n */\nexport function getScoreInfo(mnxJson: string): ScoreInfo {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.get_score_info(mnxJson);\n return JSON.parse(resultJson) as ScoreInfo;\n}\n\n/**\n * Toggle the layout-debug sidecar. When enabled, every subsequent layout\n * call emits a `layoutDebug` field on the returned `DisplayList`. Used by\n * the editor's spacing debug overlay.\n *\n * Safe to call before `initWasm()` resolves \u2014 the value is replayed onto\n * the module once it's ready.\n */\nlet pendingDebugFlag: boolean | null = null;\nexport function setEmitLayoutDebug(enabled: boolean): void {\n if (!wasmModule) {\n pendingDebugFlag = enabled;\n return;\n }\n if (typeof wasmModule.set_emit_layout_debug === \"function\") {\n wasmModule.set_emit_layout_debug(enabled);\n }\n}\n\n/** Internal \u2014 called from initWasm once the module is ready. */\nfunction _flushPendingDebugFlag(): void {\n if (pendingDebugFlag !== null && wasmModule?.set_emit_layout_debug) {\n wasmModule.set_emit_layout_debug(pendingDebugFlag);\n pendingDebugFlag = null;\n }\n}\n\nexport function getEmitLayoutDebug(): boolean {\n return Boolean(wasmModule?.get_emit_layout_debug?.());\n}\n\n/** Per-page SVG result from WASM export. */\nexport interface WasmSvgPage {\n pageNumber: number;\n svg: string;\n widthMm: number;\n heightMm: number;\n}\n\n/**\n * Export a DisplayList to per-page SVG strings via the WASM engine.\n *\n * All music glyphs and text are expanded to vector path outlines using\n * font data, producing self-contained SVGs with no external dependencies.\n *\n * @param displayListJson - Serialised DisplayList (from any compute_* function).\n * @param bravuraData - Raw bytes of Bravura.otf (SMuFL music font).\n * @param textFontData - Raw bytes of a text font (e.g. BravuraText.otf). Pass empty for fallback.\n * @returns Array of per-page SVG results.\n */\nexport function wasmExportSvg(\n displayListJson: string,\n bravuraData: Uint8Array,\n textFontData: Uint8Array,\n spatiumMm: number,\n spPixels: number,\n pageWidthMm: number,\n pageHeightMm: number,\n): WasmSvgPage[] {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.export_svg(\n displayListJson,\n bravuraData,\n textFontData,\n spatiumMm,\n spPixels,\n pageWidthMm,\n pageHeightMm,\n );\n return JSON.parse(resultJson) as WasmSvgPage[];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Stateful cached layout engine\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Opaque handle to a WASM LayoutEngine instance with measure-level cache. */\ninterface WasmLayoutEngine {\n compute_layout_cached(\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ): string;\n compute_full_score_layout_cached(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n compute_full_score_layout_cached_binary(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n relayout_retained_score_cached(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n relayout_retained_score_cached_binary(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n apply_patch_and_layout_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout_patch_frame_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n full_layout(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n invalidate_cache(): void;\n cache_stats(): number[];\n layout_metrics_json(): string;\n take_timings_json(): string | undefined;\n has_retained_score(): boolean;\n /**\n * Lever 2 step 4 (B-full): enable the per-system wholesale layout-reuse\n * store. Off by default in the engine; the factory opts in. Byte-identical\n * to the per-measure path (proven by the engine oracle).\n */\n set_system_layout_reuse?(enabled: boolean): void;\n free(): void;\n}\n\n/** Enable detailed engine phase timings in this realm's WASM instance. */\nexport function setWasmTiming(enabled: boolean): void {\n wasmModule?.set_wasm_timing?.(enabled);\n}\n\n/** Deterministic counters from the most recent retained-engine layout pass. */\nexport interface LayoutMetrics {\n resolvedCells: number;\n resolvedFullCells: number;\n widthCells: number;\n widthFullCells: number;\n freshSystems: number;\n reusedSystems: number;\n staffContentReuses: number;\n staffContentReuseRuns: number;\n staffAuxReuses: number;\n systemMeasureReuses: number;\n spannerBoundsFull: number;\n spannerBounds: number;\n mmrPlanReused: boolean;\n frameBytes: number;\n patchFrame: boolean;\n horizonChunksReused: boolean;\n horizonStaffExtentsReused: number;\n horizonTieMapsReused: number;\n cacheHits: number;\n cacheMisses: number;\n}\n\nexport const EMPTY_LAYOUT_METRICS: LayoutMetrics = {\n resolvedCells: 0,\n resolvedFullCells: 0,\n widthCells: 0,\n widthFullCells: 0,\n freshSystems: 0,\n reusedSystems: 0,\n staffContentReuses: 0,\n staffContentReuseRuns: 0,\n staffAuxReuses: 0,\n systemMeasureReuses: 0,\n spannerBoundsFull: 0,\n spannerBounds: 0,\n mmrPlanReused: false,\n frameBytes: 0,\n patchFrame: false,\n horizonChunksReused: false,\n horizonStaffExtentsReused: 0,\n horizonTieMapsReused: 0,\n cacheHits: 0,\n cacheMisses: 0,\n};\n\n/**\n * Stateful layout engine with measure-level cache.\n * Reuses natural width computations for unchanged measures across re-layouts.\n *\n * Usage:\n * ```ts\n * const engine = createCachedLayoutEngine();\n * const dl1 = engine.computeLayout(mnx, 0, sp, pw); // full computation\n * const dl2 = engine.computeLayout(mnx2, 0, sp, pw); // only changed measures recomputed\n * engine.dispose();\n * ```\n */\nexport interface CachedLayoutEngine {\n computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n ): DisplayList;\n computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link computeFullScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy,\n * avoiding a multi-megabyte JSON serialize + clone + parse on initial load. */\n computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Re-layout the already-retained Score for a different score view / config\n * WITHOUT re-parsing MNX JSON. Requires a prior full layout call; returns\n * null when no score is retained so the caller can fall back. */\n relayoutRetainedScore(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList | null;\n /** Binary sibling of {@link relayoutRetainedScore}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy.\n * Returns null when no score is retained. */\n relayoutRetainedScoreBinary(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array | null;\n /** Apply a measure-level patch and re-layout incrementally.\n * Only changed measures are re-parsed. Requires a prior full layout call. */\n applyPatchAndLayout(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link applyPatchAndLayout}: returns the raw packed\n * Float32Array (no decode) for zero-copy worker transfer. */\n applyPatchAndLayoutBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Incremental sibling of {@link applyPatchAndLayoutBinary} that returns a\n * PATCH-FRAME delta (tagged at element 0: `0.0` = full display list, `1.0` =\n * patch frame) instead of a full display list, when the paged auto-flow path\n * can produce one. The caller reassembles it against a retained per-system\n * segment list (see `PatchReconstructor`). Falls back to a tagged full frame\n * transparently for every other layout path. */\n applyPatchAndLayoutPatchFrameBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Full parse + layout with cache invalidation (for structural changes). */\n fullLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n invalidateCache(): void;\n /** Returns [hits, misses] from the last layout pass. */\n cacheStats(): [number, number];\n /** Deterministic work/transport counters from the last layout pass. */\n layoutMetrics(): LayoutMetrics;\n /** Drain the optional detailed phase timing JSON for the last patch. */\n takeTimings(): string | null;\n /** True if the engine has a retained Score (patch API available). */\n hasRetainedScore(): boolean;\n dispose(): void;\n}\n\n/**\n * Create a stateful cached layout engine.\n * Must call initWasm() first.\n */\nexport function createCachedLayoutEngine(): CachedLayoutEngine {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n // The LayoutEngine constructor is exposed via wasm_bindgen\n const EngineClass = (wasmModule as Record<string, unknown>)[\"LayoutEngine\"] as {\n new (): WasmLayoutEngine;\n };\n if (!EngineClass) {\n throw new Error(\"LayoutEngine not found in WASM module. Rebuild WASM.\");\n }\n const engine = new EngineClass();\n\n // Lever 2 step 4 (B-full): opt into the per-system wholesale layout-reuse\n // store. Byte-identical to the per-measure path (engine oracle), and ~18ms\n // faster per warm edit on large paged scores. Guarded with `?.` so an older\n // WASM build without the setter still loads.\n engine.set_system_layout_reuse?.(true);\n\n return {\n computeLayout(mnxJson, partIndex, spatium, pageWidth = 0, pageSetupJson?) {\n const resultJson = engine.compute_layout_cached(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.compute_full_score_layout_cached(\n mnxJson,\n spatium,\n pageWidth,\n pageSetupJson,\n scoreIndex,\n );\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayoutBinary(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.compute_full_score_layout_cached_binary(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n relayoutRetainedScore(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n const resultJson = engine.relayout_retained_score_cached(spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n relayoutRetainedScoreBinary(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n return engine.relayout_retained_score_cached_binary(spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayout(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n performance.mark(\"viritura:wasm-raw-start\");\n const binary = engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n performance.mark(\"viritura:wasm-raw-end\");\n performance.measure(\"viritura:wasm-raw\", \"viritura:wasm-raw-start\", \"viritura:wasm-raw-end\");\n\n performance.mark(\"viritura:displaylist-parse-start\");\n const normalized = decodeBinaryDisplayList(binary);\n performance.mark(\"viritura:displaylist-parse-end\");\n performance.measure(\n \"viritura:displaylist-parse\",\n \"viritura:displaylist-parse-start\",\n \"viritura:displaylist-parse-end\",\n );\n return normalized;\n },\n applyPatchAndLayoutBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayoutPatchFrameBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_patch_frame_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n fullLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.full_layout(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n invalidateCache() {\n engine.invalidate_cache();\n },\n cacheStats(): [number, number] {\n const stats = engine.cache_stats();\n return [stats[0] ?? 0, stats[1] ?? 0];\n },\n layoutMetrics(): LayoutMetrics {\n try {\n return JSON.parse(engine.layout_metrics_json()) as LayoutMetrics;\n } catch {\n return { ...EMPTY_LAYOUT_METRICS };\n }\n },\n takeTimings(): string | null {\n return engine.take_timings_json() ?? null;\n },\n hasRetainedScore() {\n return engine.has_retained_score();\n },\n dispose() {\n engine.free();\n },\n };\n}\n", "/**\n * Tile-based rendering cache for score canvases.\n *\n * Pre-renders the display list into fixed-size tiles at the current zoom level.\n * On scroll, blits cached tiles instead of repainting all render commands.\n * Tiles are invalidated when zoom or content changes.\n *\n * Supports dirty tile invalidation: when content changes, only tiles overlapping\n * modified measures are invalidated (via invalidateMeasures), not the entire cache.\n *\n * Performance: reduces per-frame paint from O(commands) to O(visible tiles).\n * For a 32-bar orchestral score, that's ~1500 draw calls \u2192 ~6 drawImage blits.\n */\n\nimport { paintCommand } from \"./displayListPainter\";\nimport { computeHorizonPaperGeometry, paintPaperPage, PAPER_SHADOW_MARGIN } from \"./paperPagePainter\";\nimport { splitCommandsByPage } from \"./pageCache\";\nimport type { DisplayList, RenderCommand, MeasureBounds } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { renderCommandBounds } from \"./renderCommandBounds\";\n\n/** Default tile size in CSS pixels. Larger = fewer tiles but more memory per tile. */\nexport const DEFAULT_TILE_SIZE = 512;\n\n/**\n * Width (in content px) of one horizontal command bucket for horizon view.\n * Horizon emits a single unbroken system that can be ~190k px wide; without a\n * spatial split every tile would re-scan the entire command list (O(tiles \u00D7\n * commands)). Bucketing along X lets each tile iterate only the commands whose\n * X-extent overlaps the visible band, mirroring how page view splits commands\n * by page along Y. Chosen so a tile spans only a handful of buckets across the\n * usual zoom range (a 512px tile is ~1.7k content px at the default 0.3 zoom).\n */\nconst HORIZON_BUCKET_WIDTH = 2048;\n\n/** Max cached tiles before distant ones are evicted (bounds memory for very wide horizon scores). */\nconst MAX_CACHED_TILES = 512;\n\n/** Column buffer kept around the visible range when evicting distant tiles. */\nconst TILE_EVICT_BUFFER = 8;\n\n/**\n * Horizontal spatial index over a display list's commands for horizon view.\n * `buckets[b]` holds the ascending global indices of commands whose X-extent\n * overlaps band `[minX + b*bucketWidth, minX + (b+1)*bucketWidth)`. Commands\n * spanning multiple bands appear in each overlapping bucket. Position-less\n * commands (SetOpacity \u2014 order-dependent global state) are collected separately\n * in `opacityIndices` and merged into every tile so alpha state stays correct.\n */\ninterface HorizonIndex {\n bucketWidth: number;\n minX: number;\n buckets: number[][];\n opacityIndices: number[];\n}\n\n/**\n * K-way merge of the sorted index arrays for buckets `b1..b2` plus `extra`\n * (opacity indices), producing a single ascending, de-duplicated index list so\n * commands paint in their original global order with no double-draw.\n */\nfunction mergeSortedUnique(buckets: number[][], b1: number, b2: number, extra: number[]): number[] {\n const lists: number[][] = [];\n for (let b = b1; b <= b2; b++) {\n const l = buckets[b];\n if (l && l.length > 0) lists.push(l);\n }\n if (extra.length > 0) lists.push(extra);\n if (lists.length === 0) return [];\n if (lists.length === 1) return lists[0]!;\n\n const pointers = new Array<number>(lists.length).fill(0);\n const out: number[] = [];\n let last = -1;\n for (;;) {\n let minVal = Infinity;\n let minList = -1;\n for (let k = 0; k < lists.length; k++) {\n const list = lists[k]!;\n const p = pointers[k]!;\n if (p < list.length && list[p]! < minVal) {\n minVal = list[p]!;\n minList = k;\n }\n }\n if (minList === -1) break;\n pointers[minList]!++;\n if (minVal !== last) {\n out.push(minVal);\n last = minVal;\n }\n }\n return out;\n}\n\n/**\n * Compute the actual bounding box of all render commands, extended past the\n * nominal `displayList.width`/`height` and padded so edge text and the paper\n * shadow halo aren't clipped.\n *\n * This is the single source of truth for \"how far does painted content\n * actually reach\". The tile renderer uses it to decide which tiles exist, and\n * the editor's scroll clamp uses it (via `contentSizeForMode`) so the viewport\n * can scroll to every painted pixel. Keeping both on the same function avoids\n * the bug where the scroll clamp stopped short of content that overhangs the\n * nominal dimensions (most visible at high zoom on wide horizon scores).\n */\nexport function computeDisplayListContentBounds(displayList: DisplayList): {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n} {\n let minX = 0;\n let minY = 0;\n let maxX = displayList.width;\n let maxY = displayList.height;\n\n for (const cmd of displayList.commands) {\n const b = renderCommandBounds(cmd);\n if (!b) continue;\n if (b.x < minX) minX = b.x;\n if (b.y < minY) minY = b.y;\n if (b.x2 > maxX) maxX = b.x2;\n if (b.y2 > maxY) maxY = b.y2;\n }\n\n // Generous padding so text at edges isn't clipped AND the paper shadow\n // halo gets rendered into tiles outside the page bounds. Without this,\n // the shadow falls into unallocated tiles and is invisible in\n // tile-cached mode.\n const pad = PAPER_SHADOW_MARGIN + 8;\n return { minX: minX - pad, minY: minY - pad, maxX: maxX + pad, maxY: maxY + pad };\n}\n\n/** Key for a tile at (col, row). */\nfunction tileKey(col: number, row: number): string {\n return `${col},${row}`;\n}\n\ninterface TileRenderOpts {\n displayList: DisplayList;\n zoom: number;\n dpr: number;\n glyphAtlas: GlyphAtlas | null;\n viewMode: \"page\" | \"spread\" | \"spread-h\" | \"horizon\";\n canvasBg: string;\n paperFill: string | CanvasPattern | null;\n pageStackGap: number;\n printableInsets: PageInsets;\n}\n\ninterface PageInsets {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport interface PaintFrameOpts {\n canvas: HTMLCanvasElement;\n displayList: DisplayList;\n scrollX: number;\n scrollY: number;\n zoom: number;\n version: number;\n glyphAtlas: GlyphAtlas | null;\n viewMode?: \"page\" | \"spread\" | \"spread-h\" | \"horizon\";\n canvasBg?: string;\n /** Page fill; \null paints ink only (no paper or shadow). */\n paperFill?: string | CanvasPattern | null;\n pageStackGap?: number;\n printableInsets?: PageInsets;\n}\n\nfunction paintHorizonPaper(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n paperFill: string | CanvasPattern | null,\n): void {\n if (viewMode !== \"horizon\" || paperFill === null) return;\n const paper = computeHorizonPaperGeometry(displayList);\n paintPaperPage(ctx, paper.x, paper.y, paper.width, paper.height, paperFill);\n}\n\nfunction paintPageBackgrounds(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n pageStackGap: number,\n paperFill: string | CanvasPattern | null,\n contentY: number,\n contentH: number,\n): void {\n if (viewMode !== \"page\" || paperFill === null || !displayList.pages || displayList.pages.length === 0) return;\n const pageW = displayList.width;\n for (let pi = 0; pi < displayList.pages.length; pi++) {\n const page = displayList.pages[pi];\n if (!page) continue;\n const visualY = page.yOffset + pi * pageStackGap;\n const pageBottom = visualY + page.height;\n if (pageBottom + PAPER_SHADOW_MARGIN < contentY || visualY - PAPER_SHADOW_MARGIN > contentY + contentH) continue;\n paintPaperPage(ctx, 0, visualY, pageW, page.height, paperFill);\n }\n}\n\n/**\n * Paint render commands, skipping those whose bounding box lies fully outside\n * the cull rectangle. Commands without a bounding box (e.g. `SetOpacity`) carry\n * order-dependent global state and are always painted so opacity stays correct\n * regardless of which commands get culled. Exported for direct-paint callers\n * (spread view, zoom/import frames) that bypass the tile cache.\n */\nexport function paintCommandsCulled(\n ctx: CanvasRenderingContext2D,\n commands: readonly RenderCommand[],\n glyphAtlas: GlyphAtlas | null,\n cullX1: number,\n cullX2: number,\n cullY1: number,\n cullY2: number,\n): void {\n for (const cmd of commands) {\n const b = renderCommandBounds(cmd);\n if (b && (b.x2 < cullX1 || b.x > cullX2 || b.y2 < cullY1 || b.y > cullY2)) continue;\n if (cmd.type === \"DrawGlyph\" && glyphAtlas?.isBuilt) {\n const drawn = glyphAtlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintCommand(ctx, cmd);\n }\n}\n\nexport class TileCache {\n private tiles = new Map<string, HTMLCanvasElement>();\n private tileSize: number;\n private cachedVersion = -1;\n private cachedDisplayList: DisplayList | null = null;\n private cachedZoom = -1;\n private cachedDpr = -1;\n private cachedViewMode: \"page\" | \"spread\" | \"spread-h\" | \"horizon\" = \"page\";\n private cachedCanvasBg = \"\";\n /** Last paper fill (cream string or CanvasPattern) used to render tiles.\n * Identity comparison \u2014 a CanvasPattern object stays stable per theme,\n * and a transition from cream-string to pattern triggers re-render. */\n private cachedPaperFill: string | CanvasPattern | null = \"\";\n /** Last page-stack gap (px) used. Invalidates tiles on change. */\n private cachedPageStackGap = 0;\n private cachedPrintableInsets = \"\";\n\n /** Per-page command split for page view (rebuilt when version changes). */\n private pageCommands: RenderCommand[][] | null = null;\n\n /** Horizontal command index for horizon view (rebuilt when version changes). */\n private horizonBuckets: HorizonIndex | null = null;\n\n /** Previous measure bounds for dirty tile detection. */\n private prevMeasureBounds: readonly MeasureBounds[] = [];\n\n /** Cached actual content bounds (min/max across all commands). */\n private contentBounds = { minX: 0, minY: 0, maxX: 0, maxY: 0 };\n\n /** Max new tiles to render per frame. Remaining tiles render in subsequent frames. */\n private maxNewTilesPerFrame = 24;\n\n /** Number of tiles rendered in the last paintFrame call. */\n tilesRendered = 0;\n /** Number of tiles blitted from cache in the last paintFrame call. */\n tilesCached = 0;\n /** True if there are uncached visible tiles that weren't rendered this frame. */\n hasPendingTiles = false;\n\n constructor(tileSize = DEFAULT_TILE_SIZE) {\n this.tileSize = tileSize;\n }\n\n /** Invalidate all cached tiles (e.g. on zoom change). */\n invalidate(): void {\n this.tiles.clear();\n }\n\n /** Compute the actual bounding box of all render commands, with padding. */\n private computeContentBounds(displayList: DisplayList): { minX: number; minY: number; maxX: number; maxY: number } {\n return computeDisplayListContentBounds(displayList);\n }\n\n /**\n * Invalidate only tiles overlapping changed measures.\n * Compares old and new MeasureBounds by measure_id to find dirty regions,\n * then deletes only those tiles that overlap the changed rectangles.\n */\n invalidateMeasures(oldBounds: readonly MeasureBounds[], newBounds: readonly MeasureBounds[], zoom: number): void {\n // Build map of old bounds by measure_id+partIndex for comparison\n const oldMap = new Map<string, MeasureBounds>();\n for (const mb of oldBounds) {\n const key = `${mb.measureId ?? mb.index}:${mb.partIndex}`;\n oldMap.set(key, mb);\n }\n\n // Find dirty rectangles: measures that changed position, size, or are new/removed\n const dirtyRects: Array<{ x: number; y: number; x2: number; y2: number }> = [];\n\n for (const mb of newBounds) {\n const key = `${mb.measureId ?? mb.index}:${mb.partIndex}`;\n const old = oldMap.get(key);\n if (!old || old.x !== mb.x || old.y !== mb.y || old.width !== mb.width || old.height !== mb.height) {\n // This measure changed \u2014 mark both old and new rectangles as dirty\n dirtyRects.push({ x: mb.x, y: mb.y, x2: mb.x + mb.width, y2: mb.y + mb.height });\n if (old) {\n dirtyRects.push({ x: old.x, y: old.y, x2: old.x + old.width, y2: old.y + old.height });\n }\n }\n oldMap.delete(key);\n }\n\n // Any remaining old entries were deleted measures\n for (const old of oldMap.values()) {\n dirtyRects.push({ x: old.x, y: old.y, x2: old.x + old.width, y2: old.y + old.height });\n }\n\n if (dirtyRects.length === 0) return;\n\n // Delete tiles overlapping any dirty rectangle\n const ts = this.tileSize;\n const keysToDelete: string[] = [];\n for (const [key, _tile] of this.tiles) {\n const [colStr, rowStr] = key.split(\",\");\n const col = parseInt(colStr!, 10);\n const row = parseInt(rowStr!, 10);\n // Tile covers content rect\n const tileX1 = (col * ts) / zoom;\n const tileY1 = (row * ts) / zoom;\n const tileX2 = ((col + 1) * ts) / zoom;\n const tileY2 = ((row + 1) * ts) / zoom;\n\n for (const r of dirtyRects) {\n if (tileX2 > r.x && tileX1 < r.x2 && tileY2 > r.y && tileY1 < r.y2) {\n keysToDelete.push(key);\n break;\n }\n }\n }\n\n for (const key of keysToDelete) {\n this.tiles.delete(key);\n }\n }\n\n /**\n * Render a single tile at (col, row) in tile-grid coordinates.\n */\n private renderTile(col: number, row: number, opts: TileRenderOpts): HTMLCanvasElement {\n const { displayList, zoom, dpr, glyphAtlas, viewMode, canvasBg, paperFill, pageStackGap, printableInsets } = opts;\n const ts = this.tileSize;\n const deviceTs = Math.round(ts * dpr);\n const canvas = document.createElement(\"canvas\");\n canvas.width = deviceTs;\n canvas.height = deviceTs;\n const ctx = canvas.getContext(\"2d\")!;\n\n ctx.fillStyle = canvasBg;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const contentX = (col * ts) / zoom;\n const contentY = (row * ts) / zoom;\n ctx.setTransform(dpr * zoom, 0, 0, dpr * zoom, -contentX * dpr * zoom, -contentY * dpr * zoom);\n\n const contentW = ts / zoom;\n const contentH = ts / zoom;\n ctx.save();\n ctx.beginPath();\n ctx.rect(contentX, contentY, contentW, contentH);\n ctx.clip();\n\n paintHorizonPaper(ctx, displayList, viewMode, paperFill);\n paintPageBackgrounds(ctx, displayList, viewMode, pageStackGap, paperFill, contentY, contentH);\n\n const margin = 20 / zoom;\n const usePagePaint = viewMode === \"page\" && (displayList.pages?.length ?? 0) > 0;\n if (usePagePaint && this.pageCommands) {\n this.paintStackedPageCommands(\n ctx,\n displayList,\n glyphAtlas,\n contentX,\n contentY,\n contentW,\n contentH,\n margin,\n pageStackGap,\n printableInsets,\n );\n } else if (viewMode === \"horizon\" && this.horizonBuckets) {\n this.paintHorizonBucketed(ctx, displayList, glyphAtlas, contentX, contentY, contentW, contentH, margin);\n } else {\n this.paintFlatCommands(ctx, displayList, glyphAtlas, contentX, contentY, contentW, contentH, margin);\n }\n\n ctx.restore();\n return canvas;\n }\n\n /**\n * Build the horizontal command index for horizon view. Must be called after\n * {@link computeContentBounds} has populated {@link contentBounds}.\n */\n private buildHorizonBuckets(displayList: DisplayList): HorizonIndex {\n const { minX, maxX } = this.contentBounds;\n const bucketWidth = HORIZON_BUCKET_WIDTH;\n const span = Math.max(1, maxX - minX);\n const bucketCount = Math.max(1, Math.ceil(span / bucketWidth));\n const buckets: number[][] = Array.from({ length: bucketCount }, () => []);\n const opacityIndices: number[] = [];\n const commands = displayList.commands;\n for (let i = 0; i < commands.length; i++) {\n const b = renderCommandBounds(commands[i]!);\n if (!b) {\n // Position-less (SetOpacity): order-dependent global state, kept apart\n // and merged into every tile so alpha is correct regardless of band.\n opacityIndices.push(i);\n continue;\n }\n let b1 = Math.floor((b.x - minX) / bucketWidth);\n let b2 = Math.floor((b.x2 - minX) / bucketWidth);\n if (b1 < 0) b1 = 0;\n if (b1 > bucketCount - 1) b1 = bucketCount - 1;\n if (b2 < 0) b2 = 0;\n if (b2 > bucketCount - 1) b2 = bucketCount - 1;\n for (let k = b1; k <= b2; k++) buckets[k]!.push(i);\n }\n return { bucketWidth, minX, buckets, opacityIndices };\n }\n\n /**\n * Paint one horizon tile using only the commands whose horizontal band\n * overlaps the tile, instead of scanning the entire command list.\n */\n private paintHorizonBucketed(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n const cullY1 = contentY - margin;\n const cullY2 = contentY + contentH + margin;\n const { bucketWidth, minX, buckets, opacityIndices } = this.horizonBuckets!;\n const lastBucket = buckets.length - 1;\n let b1 = Math.floor((cullX1 - minX) / bucketWidth);\n let b2 = Math.floor((cullX2 - minX) / bucketWidth);\n if (b1 < 0) b1 = 0;\n if (b2 > lastBucket) b2 = lastBucket;\n if (b2 < 0 || b1 > lastBucket) return;\n\n const indices = mergeSortedUnique(buckets, b1, b2, opacityIndices);\n if (indices.length === 0) return;\n const commands = displayList.commands;\n const tileCmds = new Array<RenderCommand>(indices.length);\n for (let j = 0; j < indices.length; j++) tileCmds[j] = commands[indices[j]!]!;\n paintCommandsCulled(ctx, tileCmds, glyphAtlas, cullX1, cullX2, cullY1, cullY2);\n }\n\n private paintStackedPageCommands(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n pageStackGap: number,\n printableInsets: PageInsets,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n for (let pi = 0; pi < this.pageCommands!.length; pi++) {\n const cmds = this.pageCommands![pi];\n if (!cmds || cmds.length === 0) continue;\n const dy = pi * pageStackGap;\n const page = displayList.pages?.[pi];\n if (!page) continue;\n const pageCullY1 = contentY - margin - dy;\n const pageCullY2 = contentY + contentH + margin - dy;\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, page.yOffset + dy, displayList.width, Math.max(0, page.height - printableInsets.bottom));\n ctx.clip();\n ctx.translate(0, dy);\n paintCommandsCulled(ctx, cmds, glyphAtlas, cullX1, cullX2, pageCullY1, pageCullY2);\n ctx.restore();\n }\n }\n\n private paintFlatCommands(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n const cullY1 = contentY - margin;\n const cullY2 = contentY + contentH + margin;\n paintCommandsCulled(ctx, displayList.commands, glyphAtlas, cullX1, cullX2, cullY1, cullY2);\n }\n\n /**\n * Paint the visible portion of the score onto the main canvas using cached tiles.\n * Call this instead of repaintCanvas for tile-cached rendering.\n *\n * @param opts.canvasBg - Canvas background color (read from CSS variable by caller)\n * @param opts.paperFill - Page fill (cream solid or CanvasPattern). A change in\n * identity invalidates the cache so tiles get re-rendered with the new\n * paper texture when it finishes loading.\n */\n paintFrame(opts: PaintFrameOpts): void {\n const {\n canvas,\n displayList,\n scrollX,\n scrollY,\n zoom,\n version,\n glyphAtlas,\n viewMode = \"page\",\n canvasBg = \"#e0e2ea\",\n paperFill = \"#FFFFFF\",\n pageStackGap = 0,\n printableInsets = { top: 0, right: 0, bottom: 0, left: 0 },\n } = opts;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n const dpr = window.devicePixelRatio || 1;\n\n // Tiles rasterize at `dpr * zoom`; bake the atlas at the same scale so\n // glyph blits are 1:1 with device pixels instead of resampled (aliased).\n glyphAtlas?.ensureDeviceScale(dpr * zoom);\n\n this.maybeInvalidateForViewChange(zoom, viewMode, dpr, canvasBg, paperFill, pageStackGap, printableInsets);\n this.maybeInvalidateForVersionChange(version, displayList, viewMode);\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = canvasBg;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.imageSmoothingEnabled = false;\n\n const range = this.computeVisibleTileRange(canvas, scrollX, scrollY, zoom, dpr, displayList, pageStackGap);\n this.blitVisibleTiles(\n ctx,\n range,\n {\n displayList,\n zoom,\n dpr,\n glyphAtlas,\n viewMode,\n canvasBg,\n paperFill,\n pageStackGap,\n printableInsets,\n },\n scrollX,\n scrollY,\n );\n }\n\n private maybeInvalidateForViewChange(\n zoom: number,\n viewMode: TileRenderOpts[\"viewMode\"],\n dpr: number,\n canvasBg: string,\n paperFill: string | CanvasPattern | null,\n pageStackGap: number,\n printableInsets: PageInsets,\n ): void {\n const printableInsetsKey = `${printableInsets.top},${printableInsets.right},${printableInsets.bottom},${printableInsets.left}`;\n if (\n zoom !== this.cachedZoom ||\n viewMode !== this.cachedViewMode ||\n dpr !== this.cachedDpr ||\n canvasBg !== this.cachedCanvasBg ||\n paperFill !== this.cachedPaperFill ||\n pageStackGap !== this.cachedPageStackGap ||\n printableInsetsKey !== this.cachedPrintableInsets\n ) {\n this.invalidate();\n this.cachedZoom = zoom;\n this.cachedViewMode = viewMode;\n this.cachedDpr = dpr;\n this.cachedCanvasBg = canvasBg;\n this.cachedPaperFill = paperFill;\n this.cachedPageStackGap = pageStackGap;\n this.cachedPrintableInsets = printableInsetsKey;\n }\n }\n\n private maybeInvalidateForVersionChange(\n version: number,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n ): void {\n // The display list is published from an async WASM layout before React has\n // necessarily committed the matching version state. Treat object identity\n // as a second revision signal so that one paint with the previous numeric\n // version can never blit tiles from the previous list.\n if (version !== this.cachedVersion || displayList !== this.cachedDisplayList) {\n this.invalidate();\n this.prevMeasureBounds = displayList.measureBounds ?? [];\n this.cachedVersion = version;\n this.cachedDisplayList = displayList;\n this.contentBounds = this.computeContentBounds(displayList);\n this.pageCommands = viewMode === \"page\" ? splitCommandsByPage(displayList).map((p) => p.commands) : null;\n this.horizonBuckets = viewMode === \"horizon\" ? this.buildHorizonBuckets(displayList) : null;\n } else {\n if (viewMode === \"page\" && !this.pageCommands) {\n this.pageCommands = splitCommandsByPage(displayList).map((p) => p.commands);\n }\n if (viewMode === \"horizon\" && !this.horizonBuckets) {\n this.horizonBuckets = this.buildHorizonBuckets(displayList);\n }\n }\n }\n\n private computeVisibleTileRange(\n canvas: HTMLCanvasElement,\n scrollX: number,\n scrollY: number,\n zoom: number,\n dpr: number,\n displayList: DisplayList,\n pageStackGap: number,\n ): {\n minCol: number;\n maxCol: number;\n minRow: number;\n maxRow: number;\n startCol: number;\n endCol: number;\n startRow: number;\n endRow: number;\n } {\n const ts = this.tileSize;\n const viewW = canvas.width / dpr;\n const viewH = canvas.height / dpr;\n const startCol = Math.floor((scrollX * zoom) / ts);\n const startRow = Math.floor((scrollY * zoom) / ts);\n const endCol = Math.ceil(((scrollX + viewW / zoom) * zoom) / ts);\n const endRow = Math.ceil(((scrollY + viewH / zoom) * zoom) / ts);\n const stackExtra = pageStackGap > 0 && displayList.pages ? (displayList.pages.length - 1) * pageStackGap : 0;\n const { minX, minY, maxX, maxY } = this.contentBounds;\n const minCol = Math.floor((minX * zoom) / ts);\n const minRow = Math.floor((minY * zoom) / ts);\n const maxCol = Math.ceil((maxX * zoom) / ts);\n const maxRow = Math.ceil(((maxY + stackExtra) * zoom) / ts);\n return { minCol, maxCol, minRow, maxRow, startCol, endCol, startRow, endRow };\n }\n\n private blitVisibleTiles(\n ctx: CanvasRenderingContext2D,\n range: ReturnType<TileCache[\"computeVisibleTileRange\"]>,\n tileOpts: TileRenderOpts,\n scrollX: number,\n scrollY: number,\n ): void {\n const ts = this.tileSize;\n const { zoom, dpr } = tileOpts;\n this.tilesRendered = 0;\n this.tilesCached = 0;\n this.hasPendingTiles = false;\n\n for (let row = Math.max(range.minRow, range.startRow); row <= Math.min(range.maxRow, range.endRow); row++) {\n for (let col = Math.max(range.minCol, range.startCol); col <= Math.min(range.maxCol, range.endCol); col++) {\n const key = tileKey(col, row);\n let tile = this.tiles.get(key);\n if (!tile) {\n if (this.tilesRendered >= this.maxNewTilesPerFrame) {\n this.hasPendingTiles = true;\n continue;\n }\n tile = this.renderTile(col, row, tileOpts);\n this.tiles.set(key, tile);\n this.tilesRendered++;\n } else {\n this.tilesCached++;\n }\n const destX = Math.round(col * ts * dpr - scrollX * zoom * dpr);\n const destY = Math.round(row * ts * dpr - scrollY * zoom * dpr);\n ctx.drawImage(tile, destX, destY);\n }\n }\n\n this.evictDistantTiles(range);\n }\n\n /**\n * Bound memory for very wide scores (horizon can be ~190k px) by dropping\n * cached tiles whose column is far outside the visible range. Evicted tiles\n * simply re-render when scrolled back into view.\n */\n private evictDistantTiles(range: ReturnType<TileCache[\"computeVisibleTileRange\"]>): void {\n if (this.tiles.size <= MAX_CACHED_TILES) return;\n const keepMin = range.startCol - TILE_EVICT_BUFFER;\n const keepMax = range.endCol + TILE_EVICT_BUFFER;\n for (const key of this.tiles.keys()) {\n const comma = key.indexOf(\",\");\n const col = parseInt(key.slice(0, comma), 10);\n if (col < keepMin || col > keepMax) this.tiles.delete(key);\n }\n }\n}\n", "import { renderCommandBounds } from \"./renderCommandBounds\";\nimport type { DisplayList } from \"./wasm\";\n\n/**\n * Paint a paper-textured page rectangle using the canonical --paper-shadow\n * recipe from packages/ui/src/tokens.css. This is the same shadow stack\n * used on palette tiles, radial menu wedges, library cards, and the Paper\n * component \u2014 so the score canvas matches the rest of the surface system.\n *\n * CSS recipe (light theme):\n * 0 0.5px 1px rgba(80, 60, 30, 0.14)\n * 0 1.5px 3px -1 rgba(120, 90, 40, 0.14)\n * 0 4px 8px -3 rgba(140,110, 50, 0.10)\n * inset 0 1px 0 rgba(255,255,255,0.9) // lit rim\n * inset 0 0 0 1px rgba(120,100, 60, 0.10) // edge stroke\n *\n * Canvas 2D shadow API has no `spread`. We approximate a negative spread\n * by shrinking the rect that *casts* the shadow (the shadow footprint\n * gets smaller while staying centered), then paint the real page fill on\n * top with shadows disabled.\n */\n\ninterface ShadowLayer {\n offsetX: number;\n offsetY: number;\n blur: number;\n spread: number; // negative = footprint smaller than page rect\n color: string;\n}\n\ninterface PaperShadowRecipe {\n cast: ShadowLayer[];\n rimColor: string; // inset 0 1px 0\n edgeStrokeColor: string; // inset 0 0 0 1px\n}\n\nconst LIGHT_RECIPE: PaperShadowRecipe = {\n cast: [\n { offsetX: 0, offsetY: 0.5, blur: 1, spread: 0, color: \"rgba(80, 60, 30, 0.14)\" },\n { offsetX: 0, offsetY: 1.5, blur: 3, spread: -1, color: \"rgba(120, 90, 40, 0.14)\" },\n { offsetX: 0, offsetY: 4, blur: 8, spread: -3, color: \"rgba(140, 110, 50, 0.10)\" },\n ],\n rimColor: \"rgba(255, 255, 255, 0.9)\",\n edgeStrokeColor: \"rgba(120, 100, 60, 0.10)\",\n};\n\nconst DARK_RECIPE: PaperShadowRecipe = {\n cast: [\n { offsetX: 0, offsetY: 0.5, blur: 1, spread: 0, color: \"rgba(0, 0, 0, 0.40)\" },\n { offsetX: 0, offsetY: 1.5, blur: 3, spread: -1, color: \"rgba(20, 14, 4, 0.40)\" },\n { offsetX: 0, offsetY: 4, blur: 8, spread: -3, color: \"rgba(30, 20, 8, 0.32)\" },\n ],\n rimColor: \"rgba(255, 235, 200, 0.10)\",\n edgeStrokeColor: \"rgba(0, 0, 0, 0.35)\",\n};\n\n/** Bounding extent (px) the shadow paints past the page rect on each side. */\nexport const PAPER_SHADOW_MARGIN = 12;\nexport const HORIZON_PAPER_PADDING = 20;\n\nexport interface HorizonPaperGeometry {\n x: number;\n y: number;\n width: number;\n height: number;\n /** Symmetric vertical extent centered on the paper and its music ink. */\n contentHeight: number;\n}\n\ninterface InkBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\nfunction inkBounds(displayList: DisplayList): InkBounds | null {\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n const preciseElementIds = new Set<string>();\n\n for (const element of displayList.elementBboxes ?? []) {\n preciseElementIds.add(element.elementId);\n minX = Math.min(minX, element.bbox.x);\n minY = Math.min(minY, element.bbox.y);\n maxX = Math.max(maxX, element.bbox.x + element.bbox.width);\n maxY = Math.max(maxY, element.bbox.y + element.bbox.height);\n }\n\n for (let index = 0; index < displayList.commands.length; index++) {\n const command = displayList.commands[index]!;\n const elementId = displayList.elementIds?.[index];\n const hasPreciseBounds = elementId != null && preciseElementIds.has(elementId);\n if (\n hasPreciseBounds &&\n (command.type === \"DrawGlyph\" || command.type === \"DrawStretchedGlyph\" || command.type === \"DrawText\")\n ) {\n continue;\n }\n const bounds = renderCommandBounds(command);\n if (!bounds) continue;\n minX = Math.min(minX, bounds.x);\n minY = Math.min(minY, bounds.y);\n maxX = Math.max(maxX, bounds.x2);\n maxY = Math.max(maxY, bounds.y2);\n }\n\n return Number.isFinite(minX) && Number.isFinite(minY) && Number.isFinite(maxX) && Number.isFinite(maxY)\n ? { minX, minY, maxX, maxY }\n : null;\n}\n\n/** Size horizon paper around actual music ink rather than nominal engine height. */\nexport function computeHorizonPaperGeometry(displayList: DisplayList): HorizonPaperGeometry {\n const musicInk = inkBounds(displayList);\n if (!musicInk) {\n return {\n x: -HORIZON_PAPER_PADDING,\n y: -HORIZON_PAPER_PADDING,\n width: displayList.width + HORIZON_PAPER_PADDING * 2,\n height: displayList.height + HORIZON_PAPER_PADDING * 2,\n contentHeight: displayList.height,\n };\n }\n\n const { minX, minY, maxX, maxY } = musicInk;\n const horizontalPadding = Math.min(minX + HORIZON_PAPER_PADDING, displayList.width + HORIZON_PAPER_PADDING - maxX);\n const verticalPadding = Math.max(HORIZON_PAPER_PADDING, horizontalPadding);\n const y = minY - verticalPadding;\n const height = maxY - minY + verticalPadding * 2;\n const paperCenter = y + height / 2;\n return {\n x: -HORIZON_PAPER_PADDING,\n y,\n width: displayList.width + HORIZON_PAPER_PADDING * 2,\n height,\n contentHeight: paperCenter > 0 ? paperCenter * 2 : displayList.height,\n };\n}\n\nfunction detectRecipe(): PaperShadowRecipe {\n if (typeof document === \"undefined\") return LIGHT_RECIPE;\n const theme = document.documentElement.dataset.theme;\n return theme === \"dark\" || theme === \"midnight\" ? DARK_RECIPE : LIGHT_RECIPE;\n}\n\n/**\n * Paint a paper page rectangle at (x, y, w, h) with the canonical paper\n * shadow stack and fill. The fill can be a solid color string or a\n * CanvasPattern (e.g. the noise-textured paper pattern).\n */\nexport function paintPaperPage(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n paperFill: string | CanvasPattern,\n): void {\n const recipe = detectRecipe();\n\n // Cast shadow layers \u2014 paint each as a shadowed fillRect whose footprint\n // is shrunk by |spread|. The fill color doesn't matter for the shadow\n // itself, but we use a neutral so any unblurred edge isn't visible\n // (the real page fill overdraws on top).\n for (const layer of recipe.cast) {\n const s = layer.spread; // typically <= 0\n const fx = x - s;\n const fy = y - s;\n const fw = w + s * 2;\n const fh = h + s * 2;\n if (fw <= 0 || fh <= 0) continue;\n ctx.save();\n ctx.shadowColor = layer.color;\n ctx.shadowBlur = layer.blur;\n ctx.shadowOffsetX = layer.offsetX;\n ctx.shadowOffsetY = layer.offsetY;\n // Fill with a transparent-ish color so the cast color reads cleanly.\n // Using paperFill here would tint the shadow; using black would\n // double-darken. A 1-alpha black on the shrunk footprint is enough\n // for the shadow API to emit pixels without leaving a visible rect.\n ctx.fillStyle = \"#000\";\n ctx.fillRect(fx, fy, fw, fh);\n ctx.restore();\n }\n\n // Real page fill \u2014 no shadow, overdraws the shadow-casting rects above.\n ctx.fillStyle = paperFill;\n ctx.fillRect(x, y, w, h);\n\n // Inset edge stroke (1px just inside the rect).\n ctx.save();\n ctx.strokeStyle = recipe.edgeStrokeColor;\n ctx.lineWidth = 1;\n ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);\n ctx.restore();\n\n // Lit rim (top 1px highlight).\n ctx.save();\n ctx.fillStyle = recipe.rimColor;\n ctx.fillRect(x, y, w, 1);\n ctx.restore();\n}\n", "/**\n * Playhead Painter \u2014 renders a vertical playhead cursor line during playback.\n *\n * Maps a beat position to an X coordinate using the layout engine's\n * MeasureBounds.beatAnchors, then draws a semi-transparent vertical line\n * spanning all staves in the current system.\n *\n * standard engraving practice draws a similar blue playhead line spanning\n * all staves in the system during playback (ScoreView::setPlayPos).\n */\n\nimport type { MeasureBounds, DisplayList } from \"./wasm\";\nimport type { StaffInfo } from \"./overlayPainter\";\n\n/** A playhead position expressed as a measure index and beat within that measure. */\nexport interface PlayheadPosition {\n /** 0-based measure index. */\n measureIndex: number;\n /** Beat position within the measure (0 = start of measure). */\n beat: number;\n}\n\n/**\n * Where the playhead was drawn, in the painter's coordinate space. Returned by\n * `paintPlayheadAtPosition` so callers can drive auto-scroll: `x` positions the\n * view horizontally, and the `yTop`/`yBottom` band lets a tall system be\n * scrolled fully into view vertically.\n */\nexport interface PlayheadDraw {\n x: number;\n yTop: number;\n yBottom: number;\n}\n\n/** Default playhead color: semi-transparent blue, matching standard practice. */\nconst PLAYHEAD_COLOR = \"rgba(33, 150, 243, 0.6)\";\n/** Default playhead line width in score-space pixels. */\nconst PLAYHEAD_WIDTH = 2;\n\n/**\n * Map a (measureIndex, beat) position to an X coordinate in score space\n * using the layout engine's beat anchors from MeasureBounds.\n *\n * Uses linear interpolation between adjacent beat anchors.\n * Returns null if the measure or beat position cannot be resolved.\n *\n * Barline smoothing: a measure's final beat anchor is the synthetic\n * `(totalBeats, rightEdge)` pair, but the next measure's beat 0 sits a\n * `prefixWidth` further right (trailing pad + barline + next prefix). Left\n * as-is, the playhead snaps across that dead zone the instant `beat` resets\n * at the boundary. When the next measure is on the same system we instead map\n * `totalBeats` to the next measure's first-content X, so the cursor glides\n * continuously across the barline over the final note's duration.\n */\nexport function beatToX(position: PlayheadPosition, measureBounds: MeasureBounds[]): number | null {\n // Find the bounds for this measure (any part index \u2014 they share X positions)\n const bounds =\n measureBounds.find((b) => b.index === position.measureIndex && b.partIndex === 0) ??\n measureBounds.find((b) => b.index === position.measureIndex);\n if (!bounds) return null;\n const endX = nextMeasureContinuityX(bounds, position.measureIndex, measureBounds);\n return interpolateBeatAnchors(bounds, position.beat, endX);\n}\n\n/**\n * The X the playhead should reach at this measure's end (`totalBeats`) so the\n * barline crossing is continuous \u2014 the next measure's first-content X, or\n * `undefined` when smoothing shouldn't apply (no next measure, or it starts a\n * new system / page so the cursor must reach this line's right edge first).\n */\nfunction nextMeasureContinuityX(\n bounds: MeasureBounds,\n measureIndex: number,\n measureBounds: MeasureBounds[],\n): number | undefined {\n const next =\n measureBounds.find((b) => b.index === measureIndex + 1 && b.partIndex === 0) ??\n measureBounds.find((b) => b.index === measureIndex + 1);\n if (!next) return undefined;\n\n // Only smooth across the barline within a system. Across a system/page break\n // the next measure is on another line, so the playhead should still finish at\n // this measure's right edge and then jump to the next line's start.\n if (bounds.systemIndex !== undefined && next.systemIndex !== undefined && next.systemIndex !== bounds.systemIndex) {\n return undefined;\n }\n\n // First-content X of the next measure: its beat-0 anchor, else its prefix end.\n const nextStart =\n next.beatAnchors && next.beatAnchors.length > 0 ? next.beatAnchors[0]![1] : next.x + next.prefixWidth;\n\n // Guard: only ever smooth forward (the next measure should be to the right).\n return nextStart > bounds.x + bounds.width ? nextStart : undefined;\n}\n\n/**\n * Interpolate beat position to X coordinate using a measure's beat anchors.\n *\n * `endX`, when provided, overrides the X of the final anchor *only when that\n * anchor is the synthetic measure-end anchor* (its beat \u2248 `totalBeats`). This\n * is how barline smoothing maps a measure's end to the next measure's start\n * without disturbing real note anchors.\n */\nfunction interpolateBeatAnchors(bounds: MeasureBounds, beat: number, endX?: number): number | null {\n const anchors = bounds.beatAnchors;\n if (!anchors || anchors.length === 0) {\n // No anchors \u2014 use linear interpolation across measure width\n if (bounds.totalBeats <= 0) return bounds.x + bounds.prefixWidth;\n const contentWidth = bounds.width - bounds.prefixWidth;\n const fraction = beat / bounds.totalBeats;\n return bounds.x + bounds.prefixWidth + fraction * contentWidth;\n }\n\n // Clamp beat to valid range\n const clampedBeat = Math.max(0, beat);\n\n // Effective X of the final anchor. The layout always appends a synthetic\n // (totalBeats, rightEdge) anchor; when `endX` is supplied and the last anchor\n // is that synthetic end (beat \u2248 totalBeats), use it so the barline crossing\n // is continuous. Real note anchors are never overridden.\n const lastIdx = anchors.length - 1;\n const lastIsMeasureEnd = Math.abs(anchors[lastIdx]![0] - bounds.totalBeats) < 1e-6;\n const lastX = endX !== undefined && lastIsMeasureEnd ? endX : anchors[lastIdx]![1];\n const xAt = (i: number): number => (i === lastIdx ? lastX : anchors[i]![1]);\n\n // Before first anchor: extrapolate left\n const first = anchors[0]!;\n if (clampedBeat <= first[0]) {\n if (anchors.length === 1) return xAt(0);\n const second = anchors[1]!;\n const pxPerBeat = second[0] !== first[0] ? (xAt(1) - first[1]) / (second[0] - first[0]) : 0;\n return first[1] + (clampedBeat - first[0]) * pxPerBeat;\n }\n\n // After last anchor: extrapolate right\n const last = anchors[lastIdx]!;\n if (clampedBeat >= last[0]) {\n if (anchors.length === 1) return lastX;\n const prev = anchors[lastIdx - 1]!;\n const pxPerBeat = last[0] !== prev[0] ? (lastX - xAt(lastIdx - 1)) / (last[0] - prev[0]) : 0;\n return lastX + (clampedBeat - last[0]) * pxPerBeat;\n }\n\n // Between anchors: linear interpolation\n for (let i = 0; i < lastIdx; i++) {\n const lo = anchors[i]!;\n const hi = anchors[i + 1]!;\n if (clampedBeat >= lo[0] && clampedBeat <= hi[0]) {\n const range = hi[0] - lo[0];\n if (range === 0) return xAt(i);\n const t = (clampedBeat - lo[0]) / range;\n return xAt(i) + t * (xAt(i + 1) - xAt(i));\n }\n }\n\n return lastX;\n}\n\n/**\n * Find the Y extent (top and bottom) of all staves that contain a given X\n * position. Groups staves into the \"system\" at that X coordinate.\n *\n * Returns null if no staves are found at the given X.\n *\n * `pageYRange` defines the vertical bounds of the page that contains the\n * cursor's measure. The upper bound (`yEnd`) should be the *next* page's\n * `yOffset` (or `+Infinity` for the last page) rather than `yOffset + height`,\n * so that systems which overflow the fixed page height are still attributed to\n * the page that contains their first stave.\n */\nexport function findSystemYExtent(\n staves: StaffInfo[],\n x: number,\n pageYRange?: { yOffset: number; yEnd: number },\n systemYRange?: { yTop: number; yBottom: number },\n): { yTop: number; yBottom: number } | null {\n // Collect staves that span this X position (with margin)\n let matching = staves.filter((s) => x >= s.x - s.spatium && x <= s.xEnd + s.spatium);\n\n // Restrict to the system's vertical band so only staves belonging to the\n // cursor's system are included \u2014 otherwise, in a part view with one staff\n // per system, every system below/above the cursor matches the X filter and\n // the playhead stretches the full page.\n //\n // `systemYRange` is derived from the cursor system's `systemIndex`, which is\n // globally unique, so it isolates the system precisely AND is immune to page\n // overflow. When it is available it fully supersedes the page-based filter\n // below: a tall orchestral system can extend past the next page's `yOffset`,\n // and applying `pageYRange` first would wrongly prune the overflowing lower\n // staves (Trombones \u2192 Piano) before this band could include them.\n if (systemYRange) {\n // Allow a small tolerance (half a spatium) on either side so staves\n // whose Y line up with the band's edge are not excluded by fp jitter.\n const tol = (matching[0]?.spatium ?? 0) * 0.5;\n matching = matching.filter((s) => s.y + s.height >= systemYRange.yTop - tol && s.y <= systemYRange.yBottom + tol);\n } else if (pageYRange) {\n // Fallback when no system band is known: filter to staves on the cursor's\n // page only. Without this, staves on different pages that share the same X\n // range would all match, causing the playhead to span the entire score.\n matching = matching.filter((s) => s.y >= pageYRange.yOffset && s.y < pageYRange.yEnd);\n }\n\n if (matching.length === 0) return null;\n\n const margin = matching[0]!.spatium * 0.5;\n const yTop = Math.min(...matching.map((s) => s.y)) - margin;\n const yBottom = Math.max(...matching.map((s) => s.y + s.height)) + margin;\n\n return { yTop, yBottom };\n}\n\n/**\n * Paint a playhead cursor line on a canvas context.\n * Coordinates are in score space \u2014 the caller must apply viewport transform first.\n *\n * @param ctx Canvas 2D context with viewport transform already applied\n * @param x X coordinate in score space\n * @param yTop Top Y of the line in score space\n * @param yBottom Bottom Y of the line in score space\n * @param color Optional override color (default: semi-transparent blue)\n * @param lineWidth Optional override width (default: 2px)\n */\nexport function paintPlayhead(\n ctx: CanvasRenderingContext2D,\n x: number,\n yTop: number,\n yBottom: number,\n color: string = PLAYHEAD_COLOR,\n lineWidth: number = PLAYHEAD_WIDTH,\n): void {\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(x, yTop);\n ctx.lineTo(x, yBottom);\n ctx.stroke();\n ctx.restore();\n}\n\n/**\n * High-level function: given a playhead position, display list, and detected\n * staves, paint the playhead cursor on the canvas.\n *\n * Returns the X coordinate of the playhead (for auto-scroll), or null if\n * the position could not be resolved.\n */\n/**\n * Find which page (if any) contains a given Y coordinate.\n */\nexport function findPageForY(\n pages: Array<{ yOffset: number; height: number }>,\n y: number,\n): { yOffset: number; height: number; index: number } | null {\n for (let i = 0; i < pages.length; i++) {\n const p = pages[i]!;\n if (y >= p.yOffset && y < p.yOffset + p.height) {\n return { yOffset: p.yOffset, height: p.height, index: i };\n }\n }\n return null;\n}\n\nexport function paintPlayheadAtPosition(\n ctx: CanvasRenderingContext2D,\n position: PlayheadPosition,\n displayList: DisplayList,\n staves: StaffInfo[],\n color?: string,\n): PlayheadDraw | null {\n if (!displayList.measureBounds || displayList.measureBounds.length === 0) {\n return null;\n }\n\n const x = beatToX(position, displayList.measureBounds);\n if (x === null) return null;\n\n // Determine the page for this measure so we only match staves on that page.\n // Use the *next* page's yOffset (or +Infinity) as the upper bound rather\n // than the current page's height, so that systems which overflow the fixed\n // page height still attribute their lower staves to the correct page.\n const pages = displayList.pages;\n let pageYRange: { yOffset: number; yEnd: number } | undefined;\n const cursorBounds =\n displayList.measureBounds.find((b) => b.index === position.measureIndex && b.partIndex === 0) ??\n displayList.measureBounds.find((b) => b.index === position.measureIndex);\n if (pages && pages.length > 0 && cursorBounds) {\n const page = findPageForY(pages, cursorBounds.y);\n if (page) {\n const next = pages[page.index + 1];\n pageYRange = {\n yOffset: page.yOffset,\n yEnd: next ? next.yOffset : Number.POSITIVE_INFINITY,\n };\n }\n }\n\n // Further restrict to the system that contains the cursor's measure.\n // Without this, every system in a single-instrument part view matches the\n // X filter (same column across systems) and the playhead spans the whole\n // page. We narrow to staves whose Y lies within the system's vertical band,\n // computed from all measure bounds sharing the cursor's `systemIndex`.\n let systemYRange: { yTop: number; yBottom: number } | undefined;\n if (cursorBounds && cursorBounds.systemIndex !== undefined) {\n const sysIdx = cursorBounds.systemIndex;\n let yTop = Number.POSITIVE_INFINITY;\n let yBottom = Number.NEGATIVE_INFINITY;\n for (const b of displayList.measureBounds) {\n if (b.systemIndex === sysIdx) {\n if (b.y < yTop) yTop = b.y;\n if (b.y + b.height > yBottom) yBottom = b.y + b.height;\n }\n }\n if (Number.isFinite(yTop) && Number.isFinite(yBottom)) {\n systemYRange = { yTop, yBottom };\n }\n }\n\n const extent = findSystemYExtent(staves, x, pageYRange, systemYRange);\n if (extent) {\n paintPlayhead(ctx, x, extent.yTop, extent.yBottom, color);\n return { x, yTop: extent.yTop, yBottom: extent.yBottom };\n }\n // Fallback: draw across entire display list height\n paintPlayhead(ctx, x, 0, displayList.height, color);\n return { x, yTop: 0, yBottom: displayList.height };\n}\n", "/**\n * Build identity and asset locations.\n *\n * The prebuilt distribution defines `__SCORE_ENGINE_BUNDLE__` (plus version\n * and commit) so the engine finds `wasm/`, `fonts/` and its worker next to the\n * bundle file. Inside Viritura's own apps these are undefined and the host's\n * asset base (Vite `BASE_URL` / `<base>`) applies.\n */\n\ndeclare const __SCORE_ENGINE_BUNDLE__: boolean | undefined;\ndeclare const __SCORE_ENGINE_COMMIT__: string | undefined;\n\n/** Keep in sync with package.json (asserted by tests). */\nexport const PACKAGE_VERSION = \"0.1.0-next.1\";\n\nconst BUNDLE_WORKER_FILE = \"score-engine.worker.js\";\n\nexport function buildCommit(): string | null {\n return typeof __SCORE_ENGINE_COMMIT__ === \"string\" && __SCORE_ENGINE_COMMIT__ ? __SCORE_ENGINE_COMMIT__ : null;\n}\n\n/** Asset base shipped alongside the prebuilt bundle, if any. */\nexport function bundledAssetBase(): string | undefined {\n if (typeof __SCORE_ENGINE_BUNDLE__ === \"undefined\" || !__SCORE_ENGINE_BUNDLE__) return undefined;\n // Held in a variable so app bundlers don't treat the directory as an asset import.\n const moduleUrl = import.meta.url;\n return new URL(\"./\", moduleUrl).href;\n}\n\n/**\n * Start the engine's layout worker from its default location. Prebuilt\n * bundles ship the worker beside `wasm/` and `fonts/`, so it follows the\n * asset base when a host serves those files from somewhere else.\n */\nexport function spawnDefaultLayoutWorker(assetBaseUrl: string): Worker {\n // Checked inline (not via a helper) so the bundle build folds the flag and\n // drops the Vite-only branch below, which app bundlers would try to resolve.\n if (typeof __SCORE_ENGINE_BUNDLE__ !== \"undefined\" && __SCORE_ENGINE_BUNDLE__) {\n return new Worker(new URL(BUNDLE_WORKER_FILE, assetBaseUrl), { type: \"module\" });\n }\n return new Worker(new URL(\"./layoutWorker.ts\", import.meta.url), { type: \"module\" });\n}\n", "/**\n * Opaque public display-list handles.\n *\n * Consumers only ever see `DisplayList` handles; the renderer's full display\n * list stays behind a WeakMap so its shape is not part of the public API.\n */\n\nimport type { DisplayList as RendererDisplayList, PageLayout } from \"@viritura/renderer\";\nimport type { DisplayList } from \"./types\";\n\nconst internals = new WeakMap<DisplayList, RendererDisplayList>();\n\n/** Page layouts of a renderer display list; unpaged layouts form one page. */\nexport function pageLayoutsOf(displayList: RendererDisplayList): readonly PageLayout[] {\n if (displayList.pages?.length) return displayList.pages;\n return [{ pageNumber: 1, systemIndices: [], yOffset: 0, height: displayList.height }];\n}\n\n/** Wrap a renderer display list in a frozen public handle. */\nexport function wrapDisplayList(displayList: RendererDisplayList): DisplayList {\n const handle = Object.freeze({\n width: displayList.width,\n height: displayList.height,\n pageCount: pageLayoutsOf(displayList).length,\n paged: Boolean(displayList.pages?.length),\n }) as unknown as DisplayList;\n internals.set(handle, displayList);\n return handle;\n}\n\n/** Resolve a public handle back to the renderer display list. */\nexport function unwrapDisplayList(handle: DisplayList): RendererDisplayList {\n const displayList = internals.get(handle);\n if (!displayList) {\n throw new TypeError(\"DisplayList was not produced by this score engine.\");\n }\n return displayList;\n}\n", "/**\n * Typed errors for @viritura/score-engine.\n *\n * Public consumers can `instanceof`-check these to differentiate failure\n * modes without parsing error message strings.\n */\n\n/** Engine asset (WASM, font) failed to load. */\nexport class EngineLoadError extends Error {\n override readonly name = \"EngineLoadError\";\n constructor(\n message: string,\n /** What failed to load: `wasm`, `font`, or `unknown`. */\n public readonly code: \"wasm\" | \"font\" | \"unknown\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n\n/** Input MNX could not be parsed (malformed JSON or invalid schema). */\nexport class ParseError extends Error {\n override readonly name = \"ParseError\";\n constructor(\n message: string,\n /** Reason: `json` (syntax), `schema` (shape), or `unsupported` (unknown feature). */\n public readonly code: \"json\" | \"schema\" | \"unsupported\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n\n/** Layout engine failed (typically WASM panic or out-of-memory). */\nexport class LayoutError extends Error {\n override readonly name = \"LayoutError\";\n constructor(\n message: string,\n /** Reason: `wasm` (engine panic), `oom`, or `unknown`. */\n public readonly code: \"wasm\" | \"oom\" | \"unknown\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n", "/**\n * Geometry queries over a computed layout: pages, systems, measures, and the\n * mapping between musical positions and page-local canvas coordinates.\n */\n\nimport {\n beatToX,\n computeHorizonPaperGeometry,\n type DisplayList as RendererDisplayList,\n type MeasureBounds,\n type PageLayout,\n} from \"@viritura/renderer\";\nimport { pageLayoutsOf } from \"./displayListHandle\";\nimport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n HorizonPaper,\n MeasureGeometry,\n PageGeometry,\n PartInfo,\n PlayheadGeometry,\n ScoreMeasurements,\n ScorePosition,\n SystemGeometry,\n} from \"./types\";\n\nfunction boundsPartId(bounds: MeasureBounds): string {\n return bounds.partId || `#${bounds.partIndex}`;\n}\n\n/** Parts in the layout, in score order. */\nfunction layoutParts(displayList: RendererDisplayList): PartInfo[] {\n if (displayList.parts?.length) {\n return displayList.parts.map((part) => ({ id: part.id, index: part.index, name: part.name }));\n }\n const seen = new Map<string, PartInfo>();\n for (const bounds of displayList.measureBounds ?? []) {\n const id = boundsPartId(bounds);\n if (!seen.has(id)) seen.set(id, { id, index: bounds.partIndex, name: \"\" });\n }\n return [...seen.values()].sort((a, b) => a.index - b.index);\n}\n\nfunction pageIndexForY(pages: readonly PageLayout[], y: number): number {\n for (let i = 0; i < pages.length; i++) {\n const page = pages[i]!;\n if (y >= page.yOffset && y < page.yOffset + page.height) return i;\n }\n return Math.max(0, pages.length - 1);\n}\n\nexport function measureLayout(displayList: RendererDisplayList): ScoreMeasurements {\n const pages: PageGeometry[] = pageLayoutsOf(displayList).map((page, index) => ({\n index,\n width: displayList.width,\n height: page.height,\n offsetY: page.yOffset,\n }));\n return {\n pageCount: pages.length,\n pages,\n totalHeight: pages.reduce((sum, page) => sum + page.height, 0),\n maxPageWidth: displayList.width,\n parts: layoutParts(displayList),\n };\n}\n\nfunction visibleBounds(displayList: RendererDisplayList): MeasureBounds[] {\n return (displayList.measureBounds ?? []).filter((bounds) => !bounds.ghostStaff && !bounds.isExpansion);\n}\n\nexport function measureGeometry(displayList: RendererDisplayList): MeasureGeometry[] {\n const pages = pageLayoutsOf(displayList);\n return visibleBounds(displayList).map((bounds) => {\n const page = pageIndexForY(pages, bounds.y);\n return {\n index: bounds.index,\n ...(bounds.measureId ? { measureId: bounds.measureId } : {}),\n partId: boundsPartId(bounds),\n staffIndex: bounds.staffIndex,\n systemIndex: bounds.systemIndex ?? 0,\n page,\n x: bounds.x,\n y: bounds.y - pages[page]!.yOffset,\n width: bounds.width,\n height: bounds.height,\n contentX: bounds.x + bounds.prefixWidth,\n totalBeats: bounds.totalBeats,\n };\n });\n}\n\nexport function systemGeometry(displayList: RendererDisplayList): SystemGeometry[] {\n const extents = new Map<number, { page: number; x1: number; y1: number; x2: number; y2: number }>();\n for (const measure of measureGeometry(displayList)) {\n const current = extents.get(measure.systemIndex);\n const x2 = measure.x + measure.width;\n const y2 = measure.y + measure.height;\n if (!current) {\n extents.set(measure.systemIndex, { page: measure.page, x1: measure.x, y1: measure.y, x2, y2 });\n continue;\n }\n current.x1 = Math.min(current.x1, measure.x);\n current.y1 = Math.min(current.y1, measure.y);\n current.x2 = Math.max(current.x2, x2);\n current.y2 = Math.max(current.y2, y2);\n }\n return [...extents.entries()]\n .sort(([a], [b]) => a - b)\n .map(([index, e]) => ({ index, page: e.page, x: e.x1, y: e.y1, width: e.x2 - e.x1, height: e.y2 - e.y1 }));\n}\n\nexport function horizonPaper(displayList: RendererDisplayList): HorizonPaper {\n const paper = computeHorizonPaperGeometry(displayList);\n return { x: paper.x, y: paper.y, width: paper.width, height: paper.height, contentHeight: paper.contentHeight };\n}\n\n/** First (top) staff of each measure for one part, in measure order. */\nfunction partMeasures(displayList: RendererDisplayList, partId: string | undefined): MeasureBounds[] {\n const bounds = visibleBounds(displayList);\n const target = partId ?? (bounds[0] ? boundsPartId(bounds[0]) : undefined);\n if (target == null) return [];\n const byMeasure = new Map<number, MeasureBounds>();\n for (const b of bounds) {\n if (boundsPartId(b) !== target) continue;\n const existing = byMeasure.get(b.index);\n if (!existing || b.staffIndex < existing.staffIndex) byMeasure.set(b.index, b);\n }\n return [...byMeasure.values()].sort((a, b) => a.index - b.index);\n}\n\nfunction xAtBeat(measures: MeasureBounds[], bounds: MeasureBounds, beatInMeasure: number): number {\n return beatToX({ measureIndex: bounds.index, beat: beatInMeasure }, measures) ?? bounds.x + bounds.prefixWidth;\n}\n\nfunction resolvePosition(\n displayList: RendererDisplayList,\n position: ScorePosition,\n): { bounds: MeasureBounds; x: number } | null {\n const measures = partMeasures(displayList, position.partId);\n if (\"measureIndex\" in position) {\n const bounds = measures.find((m) => m.index === position.measureIndex);\n return bounds ? { bounds, x: xAtBeat(measures, bounds, position.beat) } : null;\n }\n let start = 0;\n for (const bounds of measures) {\n if (position.beat < start + bounds.totalBeats) {\n return { bounds, x: xAtBeat(measures, bounds, Math.max(0, position.beat - start)) };\n }\n start += bounds.totalBeats;\n }\n return null;\n}\n\nexport function positionToCanvas(displayList: RendererDisplayList, position: ScorePosition): CanvasBeatPosition | null {\n const resolved = resolvePosition(displayList, position);\n if (!resolved) return null;\n const pages = pageLayoutsOf(displayList);\n const page = pageIndexForY(pages, resolved.bounds.y);\n return { page, x: resolved.x, y: resolved.bounds.y - pages[page]!.yOffset, height: resolved.bounds.height };\n}\n\nexport function playheadAt(displayList: RendererDisplayList, position: ScorePosition): PlayheadGeometry | null {\n const resolved = resolvePosition(displayList, position);\n if (!resolved) return null;\n const systemIndex = resolved.bounds.systemIndex ?? 0;\n let top = Number.POSITIVE_INFINITY;\n let bottom = Number.NEGATIVE_INFINITY;\n for (const bounds of visibleBounds(displayList)) {\n if ((bounds.systemIndex ?? 0) !== systemIndex) continue;\n top = Math.min(top, bounds.y);\n bottom = Math.max(bottom, bounds.y + bounds.height);\n }\n const pages = pageLayoutsOf(displayList);\n const page = pageIndexForY(pages, resolved.bounds.y);\n return { page, x: resolved.x, y: top - pages[page]!.yOffset, height: bottom - top, systemIndex };\n}\n\nfunction beatInMeasureAtX(bounds: MeasureBounds, x: number): number {\n const anchors = bounds.beatAnchors;\n if (anchors && anchors.length >= 2) {\n if (x <= anchors[0]![1]) return anchors[0]![0];\n for (let i = 0; i < anchors.length - 1; i++) {\n const [b0, x0] = anchors[i]!;\n const [b1, x1] = anchors[i + 1]!;\n if (x >= x0 && x <= x1) return b0 + (x1 > x0 ? (x - x0) / (x1 - x0) : 0) * (b1 - b0);\n }\n return anchors[anchors.length - 1]![0];\n }\n const contentWidth = bounds.width - bounds.prefixWidth;\n if (bounds.totalBeats <= 0 || contentWidth <= 0) return 0;\n return (Math.max(0, x - bounds.x - bounds.prefixWidth) / contentWidth) * bounds.totalBeats;\n}\n\nexport function canvasToBeat(\n displayList: RendererDisplayList,\n page: number,\n x: number,\n y: number,\n): CanvasBeatHit | null {\n const pages = pageLayoutsOf(displayList);\n const absY = y + (pages[page]?.yOffset ?? 0);\n const hit = visibleBounds(displayList).find(\n (b) => x >= b.x && x <= b.x + b.width && absY >= b.y && absY <= b.y + b.height,\n );\n if (!hit) return null;\n const partId = boundsPartId(hit);\n const prior = partMeasures(displayList, partId)\n .filter((b) => b.index < hit.index)\n .reduce((sum, b) => sum + b.totalBeats, 0);\n return { beat: prior + beatInMeasureAtX(hit, x), measureIndex: hit.index, partId };\n}\n", "/** Translation of public layout options into engine calls, shared by main thread and worker. */\n\nimport { LayoutError, ParseError } from \"./errors\";\nimport type { LayoutOptions } from \"./types\";\n\nexport interface LayoutArgs {\n readonly json: string;\n readonly spatium: number;\n readonly pageWidth: number;\n readonly scoreIndex: number;\n readonly pageSetupJson: string | undefined;\n}\n\nexport function mnxJson(mnx: string | object): string {\n if (typeof mnx === \"string\") return mnx;\n try {\n return JSON.stringify(mnx);\n } catch (err) {\n throw new ParseError(`MNX input is not JSON-serializable: ${messageOf(err)}`, \"json\", err);\n }\n}\n\nexport function layoutArgs(mnx: string | object, opts: LayoutOptions): LayoutArgs {\n const spatium = opts.spatium ?? 7;\n if (!(spatium > 0)) throw new RangeError(`spatium must be positive, got ${spatium}`);\n const setup = opts.pageSetup;\n return {\n json: mnxJson(mnx),\n spatium,\n pageWidth: opts.pageWidth,\n scoreIndex: opts.scoreIndex ?? 0,\n // The engine takes page geometry in staff spaces.\n pageSetupJson: setup\n ? JSON.stringify({\n page_height: setup.height / spatium,\n page_margin_top: setup.margins.top / spatium,\n page_margin_right: setup.margins.right / spatium,\n page_margin_bottom: setup.margins.bottom / spatium,\n page_margin_left: setup.margins.left / spatium,\n })\n : undefined,\n };\n}\n\nexport function messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Classify a raw engine failure. The Rust engine reports errors as strings. */\nexport function layoutFailure(err: unknown): ParseError | LayoutError {\n if (err instanceof ParseError || err instanceof LayoutError) return err;\n const msg = messageOf(err);\n if (/parse|json|schema|missing field|expected|invalid type|unknown variant/i.test(msg)) {\n return new ParseError(`Failed to parse MNX: ${msg}`, \"schema\", err);\n }\n if (/out of memory|memory access|allocation/i.test(msg)) {\n return new LayoutError(`Layout failed: ${msg}`, \"oom\", err);\n }\n return new LayoutError(`Layout failed: ${msg}`, \"wasm\", err);\n}\n", "import { decodeBinaryDisplayList } from \"@viritura/renderer\";\nimport { spawnDefaultLayoutWorker } from \"./buildInfo\";\nimport { wrapDisplayList } from \"./displayListHandle\";\nimport { EngineLoadError, LayoutError } from \"./errors\";\nimport { layoutArgs, layoutFailure } from \"./layoutRequest\";\nimport type { LayoutWorkerCommand, LayoutWorkerRequest, LayoutWorkerResponse } from \"./layoutWorkerProtocol\";\nimport type { DisplayList, LayoutOptions, LayoutWorker, LayoutWorkerOptions } from \"./types\";\n\ninterface Pending {\n resolve(data: Float32Array | undefined): void;\n reject(err: Error): void;\n}\n\nexport function createLayoutWorker(assetBaseUrl: string, opts: LayoutWorkerOptions = {}): LayoutWorker {\n const worker = opts.url ? new Worker(opts.url, { type: \"module\" }) : spawnDefaultLayoutWorker(assetBaseUrl);\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let disposed = false;\n\n const failAll = (err: Error): void => {\n for (const p of pending.values()) p.reject(err);\n pending.clear();\n };\n\n worker.onmessage = (event: MessageEvent<LayoutWorkerResponse>) => {\n const res = event.data;\n const p = pending.get(res.id);\n if (!p) return;\n pending.delete(res.id);\n if (res.ok) p.resolve(res.data);\n else if (res.kind === \"load\") p.reject(new EngineLoadError(`Layout worker failed to load: ${res.message}`, \"wasm\"));\n else p.reject(layoutFailure(new Error(res.message)));\n };\n worker.onerror = (event: ErrorEvent) => {\n event.preventDefault();\n failAll(new EngineLoadError(`Layout worker error: ${event.message || \"script failed to load\"}`, \"wasm\", event));\n };\n\n const send = (command: LayoutWorkerCommand): Promise<Float32Array | undefined> => {\n if (disposed) return Promise.reject(new LayoutError(\"Layout worker has been disposed\", \"unknown\"));\n const id = nextId++;\n return new Promise((resolve, reject) => {\n pending.set(id, { resolve, reject });\n const request: LayoutWorkerRequest = { ...command, id };\n worker.postMessage(request);\n });\n };\n\n const ready = send({ type: \"init\", assetBaseUrl });\n // Surface init failures through layout() rather than as unhandled rejections.\n ready.catch(() => undefined);\n\n return {\n async layout(mnx: string | object, layoutOpts: LayoutOptions): Promise<DisplayList> {\n const args = layoutArgs(mnx, layoutOpts);\n await ready;\n const data = await send({ type: \"layout\", args });\n if (!data) throw new LayoutError(\"Layout worker returned no data\", \"unknown\");\n return wrapDisplayList(decodeBinaryDisplayList(data));\n },\n dispose(): void {\n if (disposed) return;\n disposed = true;\n worker.terminate();\n failAll(new LayoutError(\"Layout worker has been disposed\", \"unknown\"));\n },\n };\n}\n", "/**\n * Ink-only Canvas painting for public display lists.\n *\n * `paint` never clears or scales the canvas: callers own the context\n * transform (device-pixel ratio, zoom, scroll) and any page presentation.\n */\n\nimport {\n paintCommandsCulled,\n splitCommandsByPage,\n type DisplayList as RendererDisplayList,\n type PageLayout,\n type RenderCommand,\n} from \"@viritura/renderer\";\nimport type { PaintOptions } from \"./types\";\n\ninterface PageSlice {\n readonly commands: RenderCommand[];\n readonly layout: PageLayout;\n}\n\nconst pageSlices = new WeakMap<RendererDisplayList, PageSlice[]>();\nconst recoloured = new WeakMap<readonly RenderCommand[], Map<string, RenderCommand[]>>();\n\nconst DEFAULT_INK = /^#0{3}(?:0{3})?$/i;\n\n/** Commands of one page, split once per display list. */\nfunction pageSlice(displayList: RendererDisplayList, page: number): PageSlice | undefined {\n let slices = pageSlices.get(displayList);\n if (!slices) {\n slices = splitCommandsByPage(displayList);\n pageSlices.set(displayList, slices);\n }\n return slices[page];\n}\n\n/** Substitute `ink` for default black ink, keeping explicitly coloured elements. */\nexport function withInk(commands: RenderCommand[], ink: string | undefined): RenderCommand[] {\n if (!ink) return commands;\n let byInk = recoloured.get(commands);\n if (!byInk) {\n byInk = new Map();\n recoloured.set(commands, byInk);\n }\n const cached = byInk.get(ink);\n if (cached) return cached;\n const result = commands.map((cmd) =>\n \"color\" in cmd && typeof cmd.color === \"string\" && DEFAULT_INK.test(cmd.color) ? { ...cmd, color: ink } : cmd,\n ) as RenderCommand[];\n byInk.set(ink, result);\n return result;\n}\n\n/** Recolour a whole display list (used for SVG export). */\nexport function displayListWithInk(displayList: RendererDisplayList, ink: string | undefined): RendererDisplayList {\n if (!ink) return displayList;\n return { ...displayList, commands: withInk(displayList.commands, ink) };\n}\n\nexport function paintInk(ctx: CanvasRenderingContext2D, displayList: RendererDisplayList, opts: PaintOptions): void {\n const slice = pageSlice(displayList, opts.page ?? 0);\n if (!slice) return;\n const width = displayList.width;\n const height = slice.layout.height;\n const region = opts.region ?? { x: 0, y: 0, width, height };\n ctx.save();\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (opts.background) {\n ctx.fillStyle = opts.background;\n ctx.fillRect(region.x, region.y, region.width, region.height);\n }\n // Commands are in absolute (stacked-page) coordinates; shift the page to\n // the origin so geometry and painting agree on page-local space.\n const offsetY = slice.layout.yOffset;\n ctx.translate(0, -offsetY);\n paintCommandsCulled(\n ctx,\n withInk(slice.commands, opts.ink),\n null,\n region.x,\n region.x + region.width,\n region.y + offsetY,\n region.y + offsetY + region.height,\n );\n ctx.restore();\n}\n", "import { wasmExportSvg, type DisplayList as RendererDisplayList } from \"@viritura/renderer\";\nimport { pageLayoutsOf } from \"./displayListHandle\";\nimport { displayListWithInk } from \"./paint\";\nimport type { SvgOptions } from \"./types\";\n\n/** Display-list units are CSS pixels: 96 per inch. */\nconst MM_PER_UNIT = 25.4 / 96;\n\nconst fontBytes = new Map<string, Promise<Uint8Array>>();\n\nfunction fetchFont(url: string): Promise<Uint8Array> {\n let bytes = fontBytes.get(url);\n if (!bytes) {\n bytes = fetch(url).then(async (res) => {\n if (!res.ok) throw new Error(`Failed to fetch font ${url}: ${res.status}`);\n return new Uint8Array(await res.arrayBuffer());\n });\n // Let a failed fetch be retried.\n bytes.catch(() => fontBytes.delete(url));\n fontBytes.set(url, bytes);\n }\n return bytes;\n}\n\n/**\n * Export one page as standalone SVG. Glyphs and text are converted to path\n * outlines, so the result needs no fonts.\n */\nexport async function exportPageSvg(\n displayList: RendererDisplayList,\n assetBase: string,\n opts: SvgOptions,\n): Promise<string> {\n const pageIndex = opts.page ?? 0;\n const pages = pageLayoutsOf(displayList);\n const page = pages[pageIndex];\n if (!page) throw new RangeError(`Page ${pageIndex} out of range (0-${pages.length - 1})`);\n const [bravura, text] = await Promise.all([\n fetchFont(`${assetBase}fonts/Bravura.otf`),\n fetchFont(`${assetBase}fonts/LibertinusSerif-Regular.otf`),\n ]);\n const source = displayListWithInk(displayList, opts.ink);\n const svgPages = wasmExportSvg(\n JSON.stringify(source),\n bravura,\n text,\n MM_PER_UNIT,\n 1,\n displayList.width * MM_PER_UNIT,\n page.height * MM_PER_UNIT,\n );\n const svg = svgPages[pageIndex]?.svg;\n if (svg === undefined) throw new RangeError(`Page ${pageIndex} was not exported`);\n return svg;\n}\n", "import { TileCache, type DisplayList as RendererDisplayList } from \"@viritura/renderer\";\nimport { unwrapDisplayList } from \"./displayListHandle\";\nimport { displayListWithInk } from \"./paint\";\nimport type { DisplayList, TilePaintOptions, TileRenderer } from \"./types\";\n\n/** Create a cached tile painter for large unpaged (horizon) layouts. */\nexport function createTileRenderer(): TileRenderer {\n const cache = new TileCache();\n let version = 0;\n let source: RendererDisplayList | null = null;\n let sourceInk: string | undefined;\n let painted: RendererDisplayList | null = null;\n\n return {\n paint(canvas: HTMLCanvasElement, displayList: DisplayList, opts: TilePaintOptions): boolean {\n const raw = unwrapDisplayList(displayList);\n if (raw !== source || opts.ink !== sourceInk || !painted) {\n source = raw;\n sourceInk = opts.ink;\n painted = displayListWithInk(raw, opts.ink);\n version++;\n }\n cache.paintFrame({\n canvas,\n displayList: painted,\n scrollX: opts.scrollX,\n scrollY: opts.scrollY,\n zoom: opts.zoom,\n version,\n glyphAtlas: null,\n viewMode: \"horizon\",\n canvasBg: opts.background ?? \"rgba(0,0,0,0)\",\n paperFill: null,\n });\n return cache.hasPendingTiles;\n },\n invalidate(): void {\n version++;\n cache.invalidate();\n },\n };\n}\n", "import { parseMnxUnvalidated } from \"@viritura/format\";\nimport { generateTimeline } from \"@viritura/midi\";\nimport type { TimedEvent, Timeline, TimelineOptions } from \"./timeline\";\nimport { stablePartIds } from \"./partIds\";\n\n/** Collect only IDs actually supplied by the source (the parser mints missing IDs). */\nfunction authoredEventIds(raw: unknown): Set<string> {\n const ids = new Set<string>();\n function visit(value: unknown): void {\n if (!value || typeof value !== \"object\") return;\n if (Array.isArray(value)) {\n for (const item of value) visit(item);\n return;\n }\n const item = value as Record<string, unknown>;\n if (item[\"type\"] === \"event\" && typeof item[\"id\"] === \"string\" && item[\"id\"]) ids.add(item[\"id\"]);\n for (const child of Object.values(item)) visit(child);\n }\n visit(raw);\n return ids;\n}\n\n/** Build a layout-independent timeline on the MIDI generator's exact written beat axis. */\nexport function buildTimeline(mnx: string | object, opts: TimelineOptions = {}): Timeline {\n const raw: unknown = typeof mnx === \"string\" ? JSON.parse(mnx) : mnx;\n const partIds = stablePartIds(raw);\n const sourceIds = authoredEventIds(raw);\n const midi = generateTimeline(parseMnxUnvalidated(raw), { expandRepeats: opts.repeatExpansion !== \"ignore\" });\n const tempoMap = midi.tempoMap.map((entry) => ({\n beat: (midi.measureStartBeats[entry.measureIndex] ?? 0) + entry.beatInMeasure,\n timeSeconds: entry.timeSeconds,\n bpm: entry.bpm,\n }));\n\n const events: TimedEvent[] = [];\n const open = new Map<string, number[]>();\n for (const ev of midi.events) {\n if (ev.partIndex >= partIds.length || (ev.type !== \"noteOn\" && ev.type !== \"noteOff\")) continue;\n const key = `${ev.partIndex}:${ev.midiNote}:${ev.channel}:${ev.playbackLaneId ?? \"\"}`;\n if (ev.type === \"noteOn\") {\n const beat = ev.scoreBeat ?? midi.model.beatAtTime(ev.time);\n const event: TimedEvent = {\n partId: partIds[ev.partIndex]!,\n beat,\n durationBeats: ev.scoreDurationBeats ?? 0,\n timeSeconds: ev.time,\n midiPitch: ev.midiNote,\n isRest: false,\n };\n if (ev.scoreEventId && sourceIds.has(ev.scoreEventId)) event.eventId = ev.scoreEventId;\n events.push(event);\n const queue = open.get(key) ?? [];\n queue.push(events.length - 1);\n open.set(key, queue);\n } else {\n const index = open.get(key)?.shift();\n if (index !== undefined && events[index]!.durationBeats === 0) {\n events[index]!.durationBeats = midi.model.beatAtTime(ev.time) - events[index]!.beat;\n }\n }\n }\n events.sort((a, b) => a.beat - b.beat);\n return {\n totalBeats: midi.totalBeats,\n totalSeconds: midi.duration,\n partIds,\n events,\n tempoMap,\n };\n}\n", "/**\n * @viritura/format \u2014 MNX parser and serializer.\n *\n * Converts between MNX JSON files and the @viritura/core typed model.\n */\n\nexport { parseMnx, parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./mnx\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./mnx\";\nexport { promote, promoteUnknown, promoteWithDiagnostics } from \"./mnx\";\nexport type { RawScore, PromoteOptions, PromoteResult } from \"./mnx\";\nexport { isRawScore, assertRawScore, validateRawScore, RawScoreValidationFailure } from \"./mnx\";\nexport type { RawScoreValidationError, RawScoreValidationResult } from \"./mnx\";\nexport { recoverInvalidSequences } from \"./mnx\";\nexport type { RecoveredSequence, RecoverInvalidSequencesOptions, SequenceRecoveryResult } from \"./mnx\";\nexport { MNX_SCHEMA_VERSION } from \"./mnx\";\nexport { serializeMnx } from \"./mnx\";\nexport {\n serializeEvent,\n serializeSequenceContent,\n serializeArpeggio,\n serializeNonArpeggio,\n serializeDynamicGroup,\n} from \"./mnx\";\nexport { DeltaSerializer } from \"./mnx\";\nexport type { DeltaSerializationResult } from \"./mnx\";\nexport { loadMnxFromUrl, loadMnxFromString } from \"./mnx\";\nexport { DiagnosticCollector, mnxPointer, mnxPointerChild } from \"./mnx\";\nexport type { MnxDiagnostic, DiagnosticSeverity } from \"./mnx\";\n", "/**\n * Barrel for the MNX subsystem (parser, serializer, validator, loader,\n * promote pipeline, delta serializer, diagnostic plumbing).\n *\n * Consumers should import from `@viritura/format` rather than reaching\n * into this folder directly; the package barrel re-exports through here.\n */\n\nexport { parseMnx, parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./parser\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./parser\";\nexport { promote, promoteUnknown, promoteWithDiagnostics } from \"./promote\";\nexport type { RawScore, PromoteOptions, PromoteResult } from \"./promote\";\nexport { isRawScore, assertRawScore, validateRawScore, RawScoreValidationFailure } from \"./validator\";\nexport type { RawScoreValidationError, RawScoreValidationResult } from \"./validator\";\nexport { recoverInvalidSequences } from \"./sequenceRecovery\";\nexport type { RecoveredSequence, RecoverInvalidSequencesOptions, SequenceRecoveryResult } from \"./sequenceRecovery\";\nexport { MNX_SCHEMA_VERSION } from \"./standaloneValidators\";\nexport { serializeMnx, serializeEvent, serializeSequenceContent } from \"./serializer\";\nexport { serializeArpeggio, serializeNonArpeggio, serializeDynamicGroup } from \"./serializePart\";\nexport { DeltaSerializer } from \"./deltaSerializer\";\nexport type { DeltaSerializationResult } from \"./deltaSerializer\";\nexport { loadMnxFromUrl, loadMnxFromString } from \"./loader\";\nexport { DiagnosticCollector, ptr as mnxPointer, ptrChild as mnxPointerChild } from \"./diagnostics\";\nexport type { MnxDiagnostic, DiagnosticSeverity } from \"./diagnostics\";\n", "/** Validated parser entry point and diagnostics compatibility surface. */\nexport { parseMnx } from \"./validatedParser\";\nexport { parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./parserCore\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./parserCore\";\n", "/**\n * MNX JSON Parser \u2014 schema-independent model builder.\n * Split into modules: parseGlobal, parseContent, parseParts, parseLayout\n */\n\nimport type {\n Score,\n SoundProfileAssignment,\n Support,\n ScoreMetadata,\n TextStyles,\n ChordSymbolStyle,\n GroupingDisplay,\n TimeSignatureDistribution,\n TimeSignatureGrandStaff,\n TimeSignaturePosition,\n TimeSignatureRenderStyle,\n TimeSignatureSettings,\n TimeSignatureStyles,\n VideoSyncSettings,\n VideoMediaIdentity,\n HitPoint,\n LyricWorkflow,\n LyricWorkflowSource,\n LyricWorkflowToken,\n} from \"@viritura/core\";\nimport { generateId } from \"@viritura/core\";\n\nimport type { SequenceContent } from \"@viritura/core\";\n\ntype Obj = Record<string, unknown>;\n\nimport { parseGlobalMeasures, parseGlobalLyrics, parseGlobalSounds } from \"./parseGlobal\";\nimport { parseParts } from \"./parseParts\";\nimport { parseLayoutDefinition, parseScoreDefinition } from \"./parseLayout\";\nimport type {\n SystemLayout as RawSystemLayout,\n Score as RawScoreDef,\n MeasureGlobal as RawMeasureGlobal,\n LyricsGlobal as RawLyricsGlobal,\n SoundsGlobal as RawSoundsGlobal,\n Part as RawPart,\n} from \"@viritura/core/raw\";\nimport { DiagnosticCollector, ptr, type MnxDiagnostic } from \"./diagnostics\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Main parser entry point\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Options for {@link parseMnxWithDiagnostics}. Pre-alpha; backward-compatible\n * with existing callers via {@link parseMnx} which discards diagnostics.\n */\nexport interface ParseMnxOptions {\n /**\n * When provided, the parser writes lossy-conversion diagnostics here.\n * Inspired by standard practice importer (see\n *\n */\n diagnostics?: DiagnosticCollector;\n /**\n * Optional schema validator. When provided, the parser invokes it on the\n * raw JSON and forwards each issue to {@link diagnostics} as an error with\n * code `\"schema-validation\"`. The validator owns its own engine (e.g.\n * Ajv2020) so this package stays dependency-free.\n *\n * See `apps/website/src/routes/mnx-converter/ValidationPanel.tsx` for an Ajv2020 example.\n */\n validate?: (json: unknown) => ReadonlyArray<{ pointer: string; message: string }>;\n}\n\n/**\n * Result of {@link parseMnxWithDiagnostics}.\n */\nexport interface ParseMnxResult {\n score: Score;\n diagnostics: readonly MnxDiagnostic[];\n}\n\n/**\n * Parse a raw MNX JSON object and collect diagnostics about lossy decisions\n * (unknown fields, fallback substitutions, dropped data).\n */\nexport function parseMnxWithDiagnostics(json: unknown, options: ParseMnxOptions = {}): ParseMnxResult {\n const diagnostics = options.diagnostics ?? new DiagnosticCollector();\n if (options.validate) {\n for (const issue of options.validate(json)) {\n diagnostics.error(issue.pointer, issue.message, \"schema-validation\");\n }\n }\n const score = parseMnxInternal(json, diagnostics);\n return { score, diagnostics: diagnostics.all() };\n}\n\n/** Build the Score model without loading or running the JSON Schema validator. */\nexport function parseMnxUnvalidated(json: unknown): Score {\n return parseMnxInternal(json, new DiagnosticCollector());\n}\n\nfunction parseSupport(supportObj: Obj | undefined): Support | undefined {\n if (!supportObj) return undefined;\n const support: Support = {};\n if (typeof supportObj[\"useAccidentalDisplay\"] === \"boolean\") {\n support.useAccidentalDisplay = supportObj[\"useAccidentalDisplay\"];\n }\n if (typeof supportObj[\"useBeams\"] === \"boolean\") {\n support.useBeams = supportObj[\"useBeams\"];\n }\n return support;\n}\n\nfunction parseRootMetadata(rootX: Obj | undefined): ScoreMetadata | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const m = viritura?.[\"metadata\"] as Obj | undefined;\n if (!m) return undefined;\n const metadata: ScoreMetadata = {};\n if (m[\"title\"]) metadata.title = m[\"title\"] as string;\n if (m[\"subtitle\"]) metadata.subtitle = m[\"subtitle\"] as string;\n if (m[\"composer\"]) metadata.composer = m[\"composer\"] as string;\n if (m[\"lyricist\"]) metadata.lyricist = m[\"lyricist\"] as string;\n if (m[\"arranger\"]) metadata.arranger = m[\"arranger\"] as string;\n if (m[\"copyright\"]) metadata.copyright = m[\"copyright\"] as string;\n return Object.keys(metadata).length > 0 ? metadata : undefined;\n}\n\nfunction parseRootTextStyles(rootX: Obj | undefined): TextStyles | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const ts = viritura?.[\"textStyles\"] as TextStyles | undefined;\n if (!ts || typeof ts !== \"object\" || Object.keys(ts).length === 0) return undefined;\n return ts;\n}\n\nfunction parseLyricWorkflowToken(raw: unknown): LyricWorkflowToken | null {\n if (!raw || typeof raw !== \"object\") return null;\n const value = raw as Obj;\n if (typeof value[\"id\"] !== \"string\" || value[\"id\"].length === 0) return null;\n const token: LyricWorkflowToken = { id: value[\"id\"] };\n if (typeof value[\"text\"] === \"string\") token.text = value[\"text\"];\n if (value[\"type\"] === \"start\" || value[\"type\"] === \"middle\" || value[\"type\"] === \"end\" || value[\"type\"] === \"whole\") {\n token.type = value[\"type\"];\n }\n if (value[\"skip\"] === true) token.skip = true;\n if (typeof value[\"eventId\"] === \"string\" && value[\"eventId\"].length > 0) token.eventId = value[\"eventId\"];\n if (typeof value[\"partId\"] === \"string\" && value[\"partId\"].length > 0) token.partId = value[\"partId\"];\n if (\n typeof value[\"sequenceIndex\"] === \"number\" &&\n Number.isInteger(value[\"sequenceIndex\"]) &&\n value[\"sequenceIndex\"] >= 0\n ) {\n token.sequenceIndex = value[\"sequenceIndex\"];\n }\n return token;\n}\n\nfunction parseRootLyricWorkflow(rootX: Obj | undefined): LyricWorkflow | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"lyricWorkflow\"] as Obj | undefined;\n const rawSources = raw?.[\"sources\"];\n if (!rawSources || typeof rawSources !== \"object\") return undefined;\n const sources: Record<string, LyricWorkflowSource> = {};\n for (const [key, candidate] of Object.entries(rawSources as Obj)) {\n if (!candidate || typeof candidate !== \"object\") continue;\n const value = candidate as Obj;\n if (\n typeof value[\"id\"] !== \"string\" ||\n typeof value[\"lineId\"] !== \"string\" ||\n typeof value[\"text\"] !== \"string\" ||\n !Array.isArray(value[\"tokens\"])\n ) {\n continue;\n }\n const tokens = value[\"tokens\"].map(parseLyricWorkflowToken).filter((token): token is LyricWorkflowToken => !!token);\n const source: LyricWorkflowSource = {\n id: value[\"id\"],\n lineId: value[\"lineId\"],\n text: value[\"text\"],\n tokens,\n };\n if (typeof value[\"language\"] === \"string\" && value[\"language\"].length > 0) source.language = value[\"language\"];\n sources[key] = source;\n }\n return Object.keys(sources).length > 0 ? { sources } : undefined;\n}\n\nconst RENDER_STYLES: readonly TimeSignatureRenderStyle[] = [\n \"standard\",\n \"narrow\",\n \"outsideStaff\",\n \"singleNumber\",\n \"noteValue\",\n];\nconst DISTRIBUTIONS: readonly TimeSignatureDistribution[] = [\"perStaff\", \"perGroup\"];\nconst GRAND_STAFF_OPTIONS: readonly TimeSignatureGrandStaff[] = [\"include\", \"exclude\"];\nconst POSITIONS: readonly TimeSignaturePosition[] = [\"center\", \"top\", \"bottom\", \"above\"];\nconst SENZA_MISURA_DISPLAYS = [\"open\", \"hidden\"] as const;\nconst GROUPING_DISPLAYS: readonly GroupingDisplay[] = [\"standard\", \"additive\", \"annotation\"];\n\nconst LEGACY_TIME_SIGNATURE_SETTINGS: Readonly<Record<string, TimeSignatureSettings>> = {\n normal: {},\n large: { scale: 1.5 },\n narrow: { renderStyle: \"narrow\" },\n aboveStaff: { position: \"above\", scale: 0.8 },\n spanning: { renderStyle: \"outsideStaff\", distribution: \"perGroup\", scale: 2 },\n singleNumber: { renderStyle: \"singleNumber\", scale: 2 },\n noteValue: { renderStyle: \"noteValue\" },\n};\n\nfunction enumValue<T extends string>(values: readonly T[], value: unknown): T | undefined {\n return values.find((candidate) => candidate === value);\n}\n\nfunction parseTimeSignatureSettings(value: unknown): TimeSignatureSettings | undefined {\n if (typeof value === \"string\") return LEGACY_TIME_SIGNATURE_SETTINGS[value];\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Obj;\n const settings: TimeSignatureSettings = {};\n const renderStyle = enumValue(RENDER_STYLES, raw[\"renderStyle\"]);\n const distribution = enumValue(DISTRIBUTIONS, raw[\"distribution\"]);\n const grandStaff = enumValue(GRAND_STAFF_OPTIONS, raw[\"grandStaff\"]);\n const position = enumValue(POSITIONS, raw[\"position\"]);\n const scale = raw[\"scale\"];\n const senzaMisura = enumValue(SENZA_MISURA_DISPLAYS, raw[\"senzaMisura\"]);\n const nonDefaultGroupingDisplay = enumValue(GROUPING_DISPLAYS, raw[\"nonDefaultGroupingDisplay\"]);\n if (renderStyle) settings.renderStyle = renderStyle;\n if (distribution) settings.distribution = distribution;\n if (grandStaff) settings.grandStaff = grandStaff;\n if (position) settings.position = position;\n if (typeof scale === \"number\" && Number.isFinite(scale)) {\n settings.scale = Math.min(12, Math.max(0.25, scale));\n }\n if (senzaMisura) settings.senzaMisura = senzaMisura;\n if (nonDefaultGroupingDisplay) settings.nonDefaultGroupingDisplay = nonDefaultGroupingDisplay;\n return settings;\n}\n\nfunction parseRootTimeSignatures(rootX: Obj | undefined): TimeSignatureStyles | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"timeSignatures\"] as Obj | undefined;\n if (!raw || typeof raw !== \"object\") return undefined;\n const styles: TimeSignatureStyles = {};\n const score = parseTimeSignatureSettings(raw[\"score\"]);\n const parts = parseTimeSignatureSettings(raw[\"parts\"]);\n if (score) styles.score = score;\n if (parts) styles.parts = parts;\n return Object.keys(styles).length > 0 ? styles : undefined;\n}\n\nfunction parseRootChordSymbolStyle(rootX: Obj | undefined): ChordSymbolStyle | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"chordSymbolStyle\"] as Obj | undefined;\n if (!raw) return undefined;\n const style: ChordSymbolStyle = {};\n const rootCase = enumValue([\"uppercase\", \"lowercaseMinor\"] as const, raw[\"rootCase\"]);\n const majorSeventh = enumValue([\"triangle\", \"maj\", \"M\"] as const, raw[\"majorSeventh\"]);\n const minor = enumValue([\"m\", \"min\", \"minus\", \"none\"] as const, raw[\"minor\"]);\n const diminished = enumValue([\"symbol\", \"dim\"] as const, raw[\"diminished\"]);\n const halfDiminished = enumValue([\"symbol\", \"minorFlatFive\"] as const, raw[\"halfDiminished\"]);\n const augmented = enumValue([\"plus\", \"aug\"] as const, raw[\"augmented\"]);\n const extensions = enumValue([\"superscript\", \"baseline\"] as const, raw[\"extensions\"]);\n if (rootCase) style.rootCase = rootCase;\n if (majorSeventh) style.majorSeventh = majorSeventh;\n if (minor) style.minor = minor;\n if (diminished) style.diminished = diminished;\n if (halfDiminished) style.halfDiminished = halfDiminished;\n if (augmented) style.augmented = augmented;\n if (extensions) style.extensions = extensions;\n return Object.keys(style).length > 0 ? style : undefined;\n}\n\nfunction parseRootSoundProfile(rootX: Obj | undefined): SoundProfileAssignment | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const assignment = viritura?.[\"soundProfile\"] as Obj | undefined;\n if (\n !assignment ||\n typeof assignment[\"profileId\"] !== \"string\" ||\n typeof assignment[\"profileVersion\"] !== \"number\" ||\n !Number.isInteger(assignment[\"profileVersion\"]) ||\n assignment[\"profileVersion\"] < 1 ||\n !assignment[\"parts\"] ||\n typeof assignment[\"parts\"] !== \"object\"\n ) {\n return undefined;\n }\n\n const parts: SoundProfileAssignment[\"parts\"] = {};\n for (const [partId, override] of Object.entries(assignment[\"parts\"] as Obj)) {\n const raw = override as Obj | undefined;\n const sourceId = raw?.[\"sourceId\"];\n if (typeof sourceId === \"string\" && sourceId.length > 0) {\n const entry: SoundProfileAssignment[\"parts\"][string] = { sourceId };\n const profileId = raw?.[\"profileId\"];\n if (typeof profileId === \"string\" && profileId.length > 0) entry.profileId = profileId;\n const profileVersion = raw?.[\"profileVersion\"];\n if (typeof profileVersion === \"number\" && Number.isInteger(profileVersion) && profileVersion >= 1) {\n entry.profileVersion = profileVersion;\n }\n parts[partId] = entry;\n }\n }\n return { profileId: assignment[\"profileId\"], profileVersion: assignment[\"profileVersion\"], parts };\n}\n\n/**\n * Parse `_x.viritura.videoSync`.\n *\n * Validation is strict about the offset because it is the one field that\n * silently corrupts the whole feature: a non-numeric offset that defaulted to\n * zero would place every cue at the wrong frame while looking like it worked.\n * Media identity is optional \u2014 a score can remember an offset for a picture the\n * user has not relinked yet.\n */\n/**\n * Parse `_x.viritura.videoSync.hitPoints`.\n *\n * Entries missing an id or a usable time are dropped rather than defaulted: a\n * hit at the wrong second is worse than a hit that is absent, because the\n * solver would silently write the cue against it.\n */\nfunction parseHitPoints(raw: unknown): HitPoint[] {\n if (!Array.isArray(raw)) return [];\n const out: HitPoint[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const item = entry as Obj;\n const id = item[\"id\"];\n const seconds = item[\"pictureSeconds\"];\n if (typeof id !== \"string\" || id.length === 0) continue;\n if (typeof seconds !== \"number\" || !Number.isFinite(seconds) || seconds < 0) continue;\n\n const hit: HitPoint = { id, pictureSeconds: seconds };\n const label = item[\"label\"];\n if (typeof label === \"string\" && label.length > 0) hit.label = label;\n if (item[\"locked\"] === false) hit.locked = false;\n out.push(hit);\n }\n return out;\n}\n\n/** Parse the attached picture's identity, which is optional and all-or-nothing. */\nfunction parseVideoMedia(raw: Obj | undefined): VideoMediaIdentity | undefined {\n const displayName = raw?.[\"displayName\"];\n if (typeof displayName !== \"string\" || displayName.length === 0) return undefined;\n\n const media: VideoMediaIdentity = { displayName };\n const contentHash = raw?.[\"contentHash\"];\n if (typeof contentHash === \"string\" && contentHash.length > 0) media.contentHash = contentHash;\n const demoSourceId = raw?.[\"demoSourceId\"];\n if (typeof demoSourceId === \"string\" && demoSourceId.length > 0) media.demoSourceId = demoSourceId;\n const duration = raw?.[\"durationSeconds\"];\n if (typeof duration === \"number\" && Number.isFinite(duration) && duration > 0) media.durationSeconds = duration;\n return media;\n}\n\nfunction parseRootVideoSync(rootX: Obj | undefined): VideoSyncSettings | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"videoSync\"] as Obj | undefined;\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const offset = raw[\"pictureOffsetSeconds\"];\n if (typeof offset !== \"number\" || !Number.isFinite(offset)) return undefined;\n\n const version = raw[\"version\"];\n const settings: VideoSyncSettings = {\n version: typeof version === \"number\" && Number.isInteger(version) && version >= 1 ? version : 1,\n pictureOffsetSeconds: offset,\n pictureAudioEnabled: raw[\"pictureAudioEnabled\"] === true,\n };\n\n const startTimecode = raw[\"startTimecodeSeconds\"];\n if (typeof startTimecode === \"number\" && Number.isFinite(startTimecode)) {\n settings.startTimecodeSeconds = startTimecode;\n }\n\n const frameRate = raw[\"frameRate\"];\n if (typeof frameRate === \"string\" && frameRate.length > 0) {\n settings.frameRate = frameRate;\n }\n\n const hitPoints = parseHitPoints(raw[\"hitPoints\"]);\n if (hitPoints.length > 0) settings.hitPoints = hitPoints;\n\n const media = parseVideoMedia(raw[\"media\"] as Obj | undefined);\n if (media) settings.media = media;\n\n return settings;\n}\n\nfunction validateSupportConsistency(\n score: Score,\n support: Support | undefined,\n diagnostics: DiagnosticCollector,\n): void {\n // mnx.support consistency checks (research \u00A71.5):\n // The writer's `support` block declares which authored fields are present.\n // If `useBeams === true` but no measure carries beams, or\n // `useAccidentalDisplay === true` but no note carries accidentalDisplay,\n // the document is internally inconsistent \u2014 emit an info diagnostic.\n if (!support) return;\n if (support.useBeams === true) {\n const hasBeams = score.parts.some((p) => p.measures?.some((m) => Array.isArray(m.beams) && m.beams.length > 0));\n if (!hasBeams) {\n diagnostics.info(\n ptr(\"mnx\", \"support\", \"useBeams\"),\n \"mnx.support.useBeams is true but no measure carries beams; engine will auto-beam\",\n \"support-mismatch\",\n );\n }\n }\n if (support.useAccidentalDisplay === true) {\n const hasAccDisp = score.parts.some((p) =>\n p.measures?.some((m) =>\n m.sequences?.some((seq) =>\n seq.content.some(\n (c) =>\n c.type === \"event\" && Array.isArray(c.notes) && c.notes.some((n) => n.accidentalDisplay !== undefined),\n ),\n ),\n ),\n );\n if (!hasAccDisp) {\n diagnostics.info(\n ptr(\"mnx\", \"support\", \"useAccidentalDisplay\"),\n \"mnx.support.useAccidentalDisplay is true but no note carries accidentalDisplay; engine will recompute\",\n \"support-mismatch\",\n );\n }\n }\n}\n\nfunction parseMnxInternal(json: unknown, diagnostics: DiagnosticCollector): Score {\n const obj = json as Obj;\n\n if (!obj || typeof obj !== \"object\") {\n throw new Error(\"MNX: Expected a JSON object at root\");\n }\n\n // Sanity-check for the standard top-level keys. This is deliberately a loose\n // interoperability heuristic: warn rather than rejecting the document.\n const hasStandardKeys = \"mnx\" in obj || \"global\" in obj || \"parts\" in obj;\n if (!hasStandardKeys) {\n diagnostics.warn(\n \"\",\n \"Document is missing standard MNX top-level keys (mnx/global/parts); may not be a valid MNX file\",\n \"missing-top-level\",\n );\n }\n\n const mnxMeta = obj[\"mnx\"] as Obj | undefined;\n const version = (mnxMeta?.[\"version\"] as number) ?? 1;\n\n // Sanity-check version. We do not enforce a minimum yet because real-world\n // MNX documents do not consistently carry a numeric version; warn on values\n // that look obviously off.\n if (mnxMeta && typeof mnxMeta[\"version\"] !== \"number\") {\n diagnostics.info(ptr(\"mnx\", \"version\"), \"MNX `mnx.version` is not numeric; assuming 1\", \"non-numeric-version\");\n }\n\n const support = parseSupport(mnxMeta?.[\"support\"] as Obj | undefined);\n\n const globalObj = obj[\"global\"] as Obj | undefined;\n const partsArr = obj[\"parts\"] as RawPart[] | undefined;\n const layoutsArr = obj[\"layouts\"] as unknown[] | undefined;\n const scoresArr = obj[\"scores\"] as unknown[] | undefined;\n\n const score: Score = {\n mnx: support ? { version, support } : { version },\n global: {\n measures: parseGlobalMeasures(globalObj?.[\"measures\"] as RawMeasureGlobal[] | undefined),\n },\n parts: parseParts(partsArr),\n };\n\n // Global lyrics\n if (globalObj?.[\"lyrics\"]) {\n score.global.lyrics = parseGlobalLyrics(globalObj[\"lyrics\"] as RawLyricsGlobal);\n }\n\n // Global sounds (drum-kit / GM sound registry)\n if (globalObj?.[\"sounds\"]) {\n score.global.sounds = parseGlobalSounds(globalObj[\"sounds\"] as RawSoundsGlobal);\n }\n\n // Layouts\n if (layoutsArr && layoutsArr.length > 0) {\n score.layouts = layoutsArr.map((l) => parseLayoutDefinition(l as RawSystemLayout));\n }\n\n // Scores\n if (scoresArr && scoresArr.length > 0) {\n score.scores = scoresArr.map((s) => parseScoreDefinition(s as RawScoreDef));\n }\n\n // Root-level vendor extensions: everything under `_x.viritura` on the\n // document root.\n applyRootExtensions(score, obj[\"_x\"] as Obj | undefined);\n\n // Auto-assign IDs to any elements that lack them\n assignMissingIds(score);\n\n validateSupportConsistency(score, support, diagnostics);\n\n // Unknown-field scan (research \u00A71.1 / \u00A71.7). Walks the raw JSON and emits\n // an info diagnostic for any field at a known node type that this parser\n // doesn't recognize. Nested `_x` vendor dicts are skipped \u2014 by convention\n // they may carry anything.\n scanUnknownFields(obj, diagnostics);\n\n return score;\n}\n\n/**\n * Read the `_x.viritura` root dict onto the score.\n *\n * Split out of `parseMnxInternal` for the same reason the serializer splits\n * `serializeRootExtensions`: each extension is an independent concern, and\n * folding them back into the entry point would push it past the complexity\n * budget one root extension at a time.\n */\nfunction applyRootExtensions(score: Score, rootX: Obj | undefined): void {\n const metadata = parseRootMetadata(rootX);\n if (metadata) score.metadata = metadata;\n\n const textStyles = parseRootTextStyles(rootX);\n if (textStyles) score.textStyles = textStyles;\n\n const timeSignatures = parseRootTimeSignatures(rootX);\n if (timeSignatures) score.timeSignatures = timeSignatures;\n const chordSymbolStyle = parseRootChordSymbolStyle(rootX);\n if (chordSymbolStyle) score.chordSymbolStyle = chordSymbolStyle;\n\n const soundProfile = parseRootSoundProfile(rootX);\n if (soundProfile) score.soundProfile = soundProfile;\n\n const videoSync = parseRootVideoSync(rootX);\n if (videoSync) score.videoSync = videoSync;\n\n const lyricWorkflow = parseRootLyricWorkflow(rootX);\n if (lyricWorkflow) score.lyricWorkflow = lyricWorkflow;\n}\n\n/**\n * Walk the parsed score and assign IDs to any elements that lack one.\n * Preserves existing IDs from the source file. Checks for uniqueness.\n *\n * Newly minted IDs are UUID v7 \u2014 the same generator the editor uses for\n * runtime edits (`generateId()` from `@viritura/core`). Re-parsing the\n * same source file will produce *different* IDs each time, which is fine:\n * the assigned IDs are mutated into the in-memory Score and persist via\n * the normal save path. Anything depending on parse output being\n * byte-stable across runs should diff structurally or strip IDs first.\n */\nfunction assignMissingIds(score: Score): void {\n const usedIds = new Set<string>();\n\n // Collect existing IDs first\n for (const gm of score.global.measures) {\n if (gm.id) usedIds.add(gm.id);\n }\n for (const part of score.parts) {\n if (part.id) usedIds.add(part.id);\n for (const pm of part.measures) {\n for (const seq of pm.sequences) {\n assignContentIds(seq.content, usedIds, true);\n }\n }\n }\n\n // Now assign missing IDs.\n for (const gm of score.global.measures) {\n if (!gm.id) gm.id = mintId(usedIds);\n }\n for (const part of score.parts) {\n if (!part.id) part.id = mintId(usedIds);\n for (const pm of part.measures) {\n for (const seq of pm.sequences) {\n assignContentIds(seq.content, usedIds, false);\n }\n }\n }\n}\n\nfunction mintId(usedIds: Set<string>): string {\n let id = generateId();\n while (usedIds.has(id)) id = generateId();\n usedIds.add(id);\n return id;\n}\n\n/**\n * Walk sequence content to collect or assign IDs on events and notes.\n * @param collectOnly - if true, only collect existing IDs; if false, assign missing ones.\n */\nfunction assignContentIds(content: SequenceContent[], usedIds: Set<string>, collectOnly: boolean): void {\n for (const item of content) {\n if (item.type === \"event\") {\n processEventIds(item, usedIds, collectOnly);\n } else if (item.type === \"tuplet\" || item.type === \"grace\") {\n assignContentIds(\n item.type === \"grace\" ? (item.content as SequenceContent[]) : item.content,\n usedIds,\n collectOnly,\n );\n } else if (item.type === \"tremolo\") {\n assignContentIds(item.content as SequenceContent[], usedIds, collectOnly);\n }\n }\n}\n\nfunction processEventIds(\n event: Extract<SequenceContent, { type: \"event\" }>,\n usedIds: Set<string>,\n collectOnly: boolean,\n): void {\n if (collectOnly) {\n if (event.id) usedIds.add(event.id);\n if (!event.notes) return;\n for (const n of event.notes) {\n if (n.id) usedIds.add(n.id);\n }\n return;\n }\n if (!event.id) event.id = mintId(usedIds);\n if (!event.notes) return;\n for (const n of event.notes) {\n if (!n.id) n.id = mintId(usedIds);\n }\n}\n\n// Re-export sub-parsers for direct access\n\n// ---------------------------------------------------------------------------\n// Unknown-field scan\n// ---------------------------------------------------------------------------\n// Per-node-type whitelists. Any other key at that location yields an info\n// diagnostic so importers can surface \"we silently dropped X\" to users.\n// _x (vendor extensions) is allowed everywhere by convention.\nconst KEYS = {\n root: new Set([\"mnx\", \"global\", \"parts\", \"layouts\", \"scores\", \"_x\"]),\n mnxMeta: new Set([\"version\", \"support\"]),\n support: new Set([\"useAccidentalDisplay\", \"useBeams\"]),\n global: new Set([\"measures\", \"lyrics\"]),\n globalLyrics: new Set([\"lineMetadata\", \"lineOrder\"]),\n globalMeasure: new Set([\n \"id\",\n \"number\",\n \"time\",\n \"key\",\n \"barline\",\n \"repeatStart\",\n \"repeatEnd\",\n \"ending\",\n \"tempos\",\n \"segno\",\n \"fine\",\n \"jump\",\n \"_x\",\n ]),\n time: new Set([\"count\", \"unit\", \"display\", \"_x\"]),\n key: new Set([\"fifths\", \"color\", \"_x\"]),\n barline: new Set([\"type\"]),\n ending: new Set([\"duration\", \"numbers\", \"open\", \"color\"]),\n tempo: new Set([\"bpm\", \"value\", \"location\", \"_x\"]),\n tempoNoteValue: new Set([\"base\", \"dots\"]),\n rhythmicPosition: new Set([\"fraction\"]),\n segno: new Set([\"location\", \"glyph\", \"color\"]),\n fine: new Set([\"location\", \"color\"]),\n jump: new Set([\"type\", \"location\"]),\n part: new Set([\"id\", \"name\", \"shortName\", \"staves\", \"transposition\", \"measures\", \"_x\"]),\n transposition: new Set([\"interval\", \"keyFifthsFlipAt\", \"prefersWrittenPitches\"]),\n partMeasure: new Set([\n \"sequences\",\n \"clefs\",\n \"staffConfigs\",\n \"arpeggios\",\n \"nonArpeggios\",\n \"beams\",\n \"dynamics\",\n \"ottavas\",\n \"measureRepeat\",\n \"_x\",\n ]),\n positionedStaffConfig: new Set([\"config\", \"position\", \"staff\", \"id\", \"_c\", \"_x\"]),\n staffConfig: new Set([\"lines\", \"id\", \"_c\", \"_x\"]),\n measureRepeat: new Set([\"number\", \"counter\", \"displayNumber\", \"staffPosition\", \"_c\", \"_x\"]),\n measureRepeatCounter: new Set([\"count\", \"placement\", \"_c\", \"_x\"]),\n arpeggio: new Set([\"id\", \"position\", \"span\", \"direction\", \"arrow\", \"_c\", \"_x\"]),\n nonArpeggio: new Set([\"id\", \"position\", \"span\", \"_c\", \"_x\"]),\n idPair: new Set([\"start\", \"end\"]),\n positionedClef: new Set([\"clef\", \"position\", \"staff\"]),\n clef: new Set([\"sign\", \"staffPosition\", \"color\", \"glyph\", \"octave\", \"showOctave\", \"hide\"]),\n beam: new Set([\"events\", \"beams\", \"direction\"]),\n dynamic: new Set([\"position\", \"value\", \"glyph\", \"staff\", \"voice\"]),\n ottava: new Set([\"position\", \"end\", \"value\", \"staff\", \"voice\"]),\n measureRhythmicPosition: new Set([\"measure\", \"position\"]),\n sequence: new Set([\"content\", \"fullMeasure\", \"staff\", \"voice\", \"directionHint\"]),\n fullMeasure: new Set([\"visualDuration\", \"staffPosition\"]),\n duration: new Set([\"base\", \"dots\"]),\n event: new Set([\n \"type\",\n \"id\",\n \"duration\",\n \"staff\",\n \"rest\",\n \"notes\",\n \"slurs\",\n \"markings\",\n \"fermata\",\n \"lyrics\",\n \"stemDirection\",\n \"smufl\",\n \"_x\",\n ]),\n rest: new Set([\"staffPosition\"]),\n note: new Set([\"pitch\", \"id\", \"ties\", \"accidentalDisplay\", \"written\"]),\n pitch: new Set([\"step\", \"octave\", \"alter\"]),\n tie: new Set([\"target\", \"targetType\", \"side\", \"lv\"]),\n accidentalDisplay: new Set([\"show\", \"force\", \"enclosure\"]),\n accidentalEnclosure: new Set([\"symbol\"]),\n written: new Set([\"diatonicDelta\"]),\n slur: new Set([\"target\", \"side\", \"sideEnd\", \"lineType\", \"startNote\", \"endNote\"]),\n markings: new Set([\n \"staccato\",\n \"staccatissimo\",\n \"spiccato\",\n \"staccatissimoWedge\",\n \"tenuto\",\n \"accent\",\n \"strongAccent\",\n \"tremolo\",\n \"softAccent\",\n \"stress\",\n \"unstress\",\n \"breath\",\n \"bowDirection\",\n \"_x\",\n ]),\n tuplet: new Set([\"type\", \"inner\", \"outer\", \"content\", \"bracket\", \"showNumber\", \"showValue\", \"placement\"]),\n tupletDuration: new Set([\"duration\", \"multiple\"]),\n grace: new Set([\"type\", \"content\", \"graceType\", \"slash\", \"color\"]),\n multiTremolo: new Set([\"type\", \"content\", \"marks\", \"outer\"]),\n space: new Set([\"type\", \"duration\"]),\n layoutDefinition: new Set([\"id\", \"content\"]),\n layoutGroup: new Set([\"type\", \"content\", \"symbol\", \"label\", \"barlineStyle\"]),\n layoutStaff: new Set([\"type\", \"sources\", \"label\", \"labelref\"]),\n layoutSource: new Set([\"part\", \"staff\", \"stem\", \"voice\", \"labelref\"]),\n scoreDefinition: new Set([\"name\", \"layout\", \"multimeasureRests\", \"pages\", \"useWritten\", \"_x\"]),\n multimeasureRest: new Set([\"start\", \"duration\"]),\n page: new Set([\"systems\"]),\n system: new Set([\"measure\", \"layout\", \"layoutChanges\"]),\n layoutChange: new Set([\"layout\", \"location\"]),\n};\n\nfunction scanUnknownFields(json: unknown, dx: DiagnosticCollector): void {\n if (!json || typeof json !== \"object\") return;\n const root = json as Obj;\n emitUnknown(root, KEYS.root, \"\", dx);\n\n const mnxMeta = root[\"mnx\"];\n if (mnxMeta && typeof mnxMeta === \"object\") {\n emitUnknown(mnxMeta as Obj, KEYS.mnxMeta, ptr(\"mnx\"), dx);\n const support = (mnxMeta as Obj)[\"support\"];\n if (support && typeof support === \"object\") {\n emitUnknown(support as Obj, KEYS.support, ptr(\"mnx\", \"support\"), dx);\n }\n }\n\n const global = root[\"global\"];\n if (global && typeof global === \"object\") {\n emitUnknown(global as Obj, KEYS.global, ptr(\"global\"), dx);\n const measures = (global as Obj)[\"measures\"];\n if (Array.isArray(measures)) {\n measures.forEach((gm, i) => scanGlobalMeasure(gm as Obj, ptr(\"global\", \"measures\", i), dx));\n }\n }\n\n const parts = root[\"parts\"];\n if (Array.isArray(parts)) {\n parts.forEach((p, i) => scanPart(p as Obj, ptr(\"parts\", i), dx));\n }\n\n const layouts = root[\"layouts\"];\n if (Array.isArray(layouts)) {\n layouts.forEach((l, i) => scanLayout(l as Obj, ptr(\"layouts\", i), dx));\n }\n\n const scores = root[\"scores\"];\n if (Array.isArray(scores)) {\n scores.forEach((s, i) => {\n if (s && typeof s === \"object\") {\n emitUnknown(s as Obj, KEYS.scoreDefinition, ptr(\"scores\", i), dx);\n }\n });\n }\n}\n\nfunction emitUnknown(obj: Obj, allowed: Set<string>, parentPtr: string, dx: DiagnosticCollector): void {\n for (const k of Object.keys(obj)) {\n if (allowed.has(k)) continue;\n if (k.startsWith(\"_\")) continue; // _x and friends are vendor-private\n dx.info(parentPtr + ptr(k), `Unknown field \"${k}\" dropped during MNX parse`, \"unknown-field\");\n }\n}\n\nfunction scanGlobalMeasure(gm: Obj, p: string, dx: DiagnosticCollector): void {\n if (!gm || typeof gm !== \"object\") return;\n emitUnknown(gm, KEYS.globalMeasure, p, dx);\n const time = gm[\"time\"] as Obj | undefined;\n if (time) emitUnknown(time, KEYS.time, p + ptr(\"time\"), dx);\n const key = gm[\"key\"] as Obj | undefined;\n if (key) emitUnknown(key, KEYS.key, p + ptr(\"key\"), dx);\n const barline = gm[\"barline\"] as Obj | undefined;\n if (barline) emitUnknown(barline, KEYS.barline, p + ptr(\"barline\"), dx);\n const ending = gm[\"ending\"] as Obj | undefined;\n if (ending) emitUnknown(ending, KEYS.ending, p + ptr(\"ending\"), dx);\n const tempos = gm[\"tempos\"];\n if (Array.isArray(tempos)) {\n tempos.forEach((t, i) => {\n if (t && typeof t === \"object\") {\n emitUnknown(t as Obj, KEYS.tempo, p + ptr(\"tempos\", i), dx);\n }\n });\n }\n const segno = gm[\"segno\"] as Obj | undefined;\n if (segno) emitUnknown(segno, KEYS.segno, p + ptr(\"segno\"), dx);\n const fine = gm[\"fine\"] as Obj | undefined;\n if (fine) emitUnknown(fine, KEYS.fine, p + ptr(\"fine\"), dx);\n const jump = gm[\"jump\"] as Obj | undefined;\n if (jump) emitUnknown(jump, KEYS.jump, p + ptr(\"jump\"), dx);\n}\n\nfunction scanPart(part: Obj, p: string, dx: DiagnosticCollector): void {\n if (!part || typeof part !== \"object\") return;\n emitUnknown(part, KEYS.part, p, dx);\n const trans = part[\"transposition\"] as Obj | undefined;\n if (trans) emitUnknown(trans, KEYS.transposition, p + ptr(\"transposition\"), dx);\n const measures = part[\"measures\"];\n if (Array.isArray(measures)) {\n measures.forEach((pm, i) => scanPartMeasure(pm as Obj, p + ptr(\"measures\", i), dx));\n }\n}\n\nfunction scanPartMeasure(pm: Obj, p: string, dx: DiagnosticCollector): void {\n if (!pm || typeof pm !== \"object\") return;\n emitUnknown(pm, KEYS.partMeasure, p, dx);\n const sequences = pm[\"sequences\"];\n if (Array.isArray(sequences)) {\n sequences.forEach((s, i) => scanSequence(s as Obj, p + ptr(\"sequences\", i), dx));\n }\n const beams = pm[\"beams\"];\n if (Array.isArray(beams)) {\n beams.forEach((b, i) => {\n if (b && typeof b === \"object\") {\n emitUnknown(b as Obj, KEYS.beam, p + ptr(\"beams\", i), dx);\n }\n });\n }\n const arpeggios = pm[\"arpeggios\"];\n if (Array.isArray(arpeggios)) {\n arpeggios.forEach((a, i) => scanArpeggioObject(a as Obj, p + ptr(\"arpeggios\", i), dx));\n }\n const nonArpeggios = pm[\"nonArpeggios\"];\n if (Array.isArray(nonArpeggios)) {\n nonArpeggios.forEach((a, i) => scanNonArpeggioObject(a as Obj, p + ptr(\"nonArpeggios\", i), dx));\n }\n const measureRepeat = pm[\"measureRepeat\"] as Obj | undefined;\n if (measureRepeat && typeof measureRepeat === \"object\") {\n emitUnknown(measureRepeat, KEYS.measureRepeat, p + ptr(\"measureRepeat\"), dx);\n const counter = measureRepeat[\"counter\"] as Obj | undefined;\n if (counter && typeof counter === \"object\") {\n emitUnknown(counter, KEYS.measureRepeatCounter, p + ptr(\"measureRepeat\") + ptr(\"counter\"), dx);\n }\n const staffConfigs = pm[\"staffConfigs\"];\n if (Array.isArray(staffConfigs)) {\n staffConfigs.forEach((staffConfig, i) => {\n if (!staffConfig || typeof staffConfig !== \"object\") return;\n emitUnknown(staffConfig as Obj, KEYS.positionedStaffConfig, p + ptr(\"staffConfigs\", i), dx);\n const config = (staffConfig as Obj)[\"config\"] as Obj | undefined;\n if (config && typeof config === \"object\") {\n emitUnknown(config, KEYS.staffConfig, p + ptr(\"staffConfigs\", i) + ptr(\"config\"), dx);\n }\n });\n }\n }\n}\n\nfunction scanArpeggioObject(obj: Obj, p: string, dx: DiagnosticCollector): void {\n if (!obj || typeof obj !== \"object\") return;\n emitUnknown(obj, KEYS.arpeggio, p, dx);\n const position = obj[\"position\"] as Obj | undefined;\n if (position) emitUnknown(position, KEYS.rhythmicPosition, p + ptr(\"position\"), dx);\n const span = obj[\"span\"] as Obj | undefined;\n if (span) emitUnknown(span, KEYS.idPair, p + ptr(\"span\"), dx);\n}\n\nfunction scanNonArpeggioObject(obj: Obj, p: string, dx: DiagnosticCollector): void {\n if (!obj || typeof obj !== \"object\") return;\n emitUnknown(obj, KEYS.nonArpeggio, p, dx);\n const position = obj[\"position\"] as Obj | undefined;\n if (position) emitUnknown(position, KEYS.rhythmicPosition, p + ptr(\"position\"), dx);\n const span = obj[\"span\"] as Obj | undefined;\n if (span) emitUnknown(span, KEYS.idPair, p + ptr(\"span\"), dx);\n}\n\nfunction scanSequence(seq: Obj, p: string, dx: DiagnosticCollector): void {\n if (!seq || typeof seq !== \"object\") return;\n emitUnknown(seq, KEYS.sequence, p, dx);\n const content = seq[\"content\"];\n if (Array.isArray(content)) {\n content.forEach((c, i) => scanContentItem(c as Obj, p + ptr(\"content\", i), dx));\n }\n}\n\nfunction scanContentItem(c: Obj, p: string, dx: DiagnosticCollector): void {\n if (!c || typeof c !== \"object\") return;\n // MNX `type` is required on tuplet/grace/space/tremolo, but optional on event\n // (event is the default content kind when no discriminator is present).\n const type = c[\"type\"] ?? \"event\";\n if (type === \"event\") {\n emitUnknown(c, KEYS.event, p, dx);\n const notes = c[\"notes\"];\n if (Array.isArray(notes)) {\n notes.forEach((n, i) => {\n if (n && typeof n === \"object\") {\n emitUnknown(n as Obj, KEYS.note, p + ptr(\"notes\", i), dx);\n }\n });\n }\n const markings = c[\"markings\"] as Obj | undefined;\n if (markings) emitUnknown(markings, KEYS.markings, p + ptr(\"markings\"), dx);\n } else if (type === \"tuplet\") {\n emitUnknown(c, KEYS.tuplet, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"grace\") {\n emitUnknown(c, KEYS.grace, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"tremolo\") {\n emitUnknown(c, KEYS.multiTremolo, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"space\") {\n emitUnknown(c, KEYS.space, p, dx);\n }\n}\n\nfunction scanLayout(l: Obj, p: string, dx: DiagnosticCollector): void {\n if (!l || typeof l !== \"object\") return;\n emitUnknown(l, KEYS.layoutDefinition, p, dx);\n const content = l[\"content\"];\n if (Array.isArray(content)) {\n content.forEach((it, i) => scanLayoutItem(it as Obj, p + ptr(\"content\", i), dx));\n }\n}\n\nfunction scanLayoutItem(it: Obj, p: string, dx: DiagnosticCollector): void {\n if (!it || typeof it !== \"object\") return;\n const type = it[\"type\"];\n if (type === \"group\") {\n emitUnknown(it, KEYS.layoutGroup, p, dx);\n const inner = it[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanLayoutItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"staff\") {\n emitUnknown(it, KEYS.layoutStaff, p, dx);\n const sources = it[\"sources\"];\n if (Array.isArray(sources)) {\n sources.forEach((s, i) => {\n if (s && typeof s === \"object\") {\n emitUnknown(s as Obj, KEYS.layoutSource, p + ptr(\"sources\", i), dx);\n }\n });\n }\n }\n}\n", "// MNX Parser \u2014 parseGlobal (split from parser.ts)\n//\n// Schema-defined inputs (sounds, lyrics, measure-global, time, key,\n// barline, ending, tempo, segno, fine, jump, dynamic-group, ottava,\n// rhythmic-position, measure-rhythmic-position, repeat-start, repeat-end)\n// now consume the generated Raw* types \u2014 schema drift surfaces as a\n// compile error here.\n//\n// Viritura vendor extensions (pedal, chordSymbol, textExpression,\n// coda) are NOT defined in the MNX schema, so their inputs remain\n// Record<string, unknown>. See docs/spec/viritura-extensions.md for\n// the layered types.\n\nimport type { GlobalLyrics, LyricLineMetadataEntry } from \"@viritura/core\";\nimport type { Sound } from \"@viritura/core\";\nimport { generateId } from \"@viritura/core\";\nimport type {\n GlobalMeasure,\n RhythmicPosition,\n Tempo,\n TempoNoteValue,\n DynamicGroup,\n DynamicGroupBase,\n MeasureRhythmicPosition,\n Ottava,\n Pedal,\n PedalType,\n PedalLineStyle,\n Segno,\n Fine,\n Jump,\n Coda,\n GradualTempo,\n GradualTempoKind,\n RepeatStart,\n RepeatEnd,\n Ending,\n TextExpression,\n ExpressionPlacement,\n ChordSymbol,\n ChordRoot,\n} from \"@viritura/core\";\nimport type { TimeSignature } from \"@viritura/core\";\nimport type { KeySignature } from \"@viritura/core\";\nimport type { Barline } from \"@viritura/core\";\nimport type { NoteValueBase, BarlineType } from \"@viritura/core\";\n\nimport type {\n SoundsGlobal as RawSoundsGlobal,\n LyricsGlobal as RawLyricsGlobal,\n MeasureGlobal as RawMeasureGlobal,\n RhythmicPosition as RawRhythmicPosition,\n MeasureRhythmicPosition as RawMeasureRhythmicPosition,\n Tempo as RawTempo,\n NoteValue as RawNoteValue,\n DynamicGroup as RawDynamicGroup,\n Ottava as RawOttava,\n Segno as RawSegno,\n Fine as RawFine,\n Jump as RawJump,\n Ending as RawEnding,\n RepeatStart as RawRepeatStart,\n RepeatEnd as RawRepeatEnd,\n Time as RawTime,\n Key as RawKey,\n Barline as RawBarline,\n} from \"@viritura/core/raw\";\nimport type {\n DynamicGroupExtensions as RawDynamicGroupExt,\n MeasureGlobalExtensions as RawMeasureGlobalExt,\n TimeExtensions as RawTimeExtensions,\n Pedal as RawPedal,\n ChordSymbol as RawChordSymbol,\n ChordRoot as RawChordRoot,\n TextExpression as RawTextExpression,\n Coda as RawCoda,\n Jump as RawVendorJump,\n GradualTempo as RawGradualTempo,\n} from \"@viritura/core/raw-viritura\";\n\n/** Untyped vendor-extension payload \u2014 used only for fields outside the\n * extensions schema (e.g. legacy `times` on repeat-start, atonal key flag). */\ntype VendorObj = Record<string, unknown>;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global sounds (drum-kit / GM sound registry)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalSounds(raw: RawSoundsGlobal): Record<string, Sound> {\n const out: Record<string, Sound> = {};\n for (const key of Object.keys(raw)) {\n const s = raw[key]!;\n const sound: Sound = {};\n if (typeof s.midiNumber === \"number\") sound.midiNumber = s.midiNumber;\n if (typeof s.name === \"string\") sound.name = s.name;\n if (typeof s.id === \"string\") sound.id = s.id;\n out[key] = sound;\n }\n return out;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global lyrics\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalLyrics(raw: RawLyricsGlobal): GlobalLyrics {\n const result: GlobalLyrics = {};\n if (raw.lineMetadata) {\n const entries: Record<string, LyricLineMetadataEntry> = {};\n for (const key of Object.keys(raw.lineMetadata)) {\n const entry = raw.lineMetadata[key]!;\n const parsed: LyricLineMetadataEntry = {};\n if (entry.label) parsed.label = entry.label;\n if (entry.lang) parsed.lang = entry.lang;\n entries[key] = parsed;\n }\n result.lineMetadata = entries;\n }\n if (raw.lineOrder) {\n result.lineOrder = raw.lineOrder;\n }\n return result;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global measures\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalMeasures(arr: RawMeasureGlobal[] | undefined): GlobalMeasure[] {\n if (!arr) return [];\n return arr.map(parseGlobalMeasure);\n}\n\nfunction parseGlobalMeasure(raw: RawMeasureGlobal): GlobalMeasure {\n const measure: GlobalMeasure = {};\n\n if (raw.id) measure.id = raw.id;\n if (raw.number !== undefined) measure.number = raw.number;\n\n if (raw.time) measure.time = parseTimeSignature(raw.time);\n if (raw.key) measure.key = parseKeySignature(raw.key);\n if (raw.barline) measure.barline = parseBarline(raw.barline);\n\n if (raw.repeatStart !== undefined) {\n measure.repeatStart = parseRepeatStart(raw.repeatStart);\n }\n if (raw.repeatEnd !== undefined) {\n measure.repeatEnd = parseRepeatEnd(raw.repeatEnd);\n }\n if (raw.ending !== undefined) {\n measure.ending = parseEnding(raw.ending);\n }\n if (raw.tempos) {\n measure.tempos = raw.tempos.map(parseTempo);\n }\n if (raw.segno) measure.segno = parseSegno(raw.segno);\n if (raw.fine) measure.fine = parseFine(raw.fine);\n if (raw.jump) measure.jump = parseJump(raw.jump);\n\n // _x.viritura vendor extensions on the global measure\n const viritura = raw._x?.[\"viritura\"] as RawMeasureGlobalExt | undefined;\n if (viritura) {\n if (viritura.senzaMisura && measure.time) measure.time.display = \"senzaMisura\";\n if (viritura.rehearsalMark) {\n const rm = viritura.rehearsalMark;\n measure.rehearsalMark = { text: rm.text };\n if (rm.style) measure.rehearsalMark.style = rm.style;\n applyManualPlacement(measure.rehearsalMark, rm as VendorObj);\n }\n if (viritura.coda) {\n measure.coda = parseCoda(viritura.coda);\n }\n if (viritura.jump) {\n measure.jump = parseJump(viritura.jump);\n }\n if (viritura.gradualTempo) {\n measure.gradualTempo = parseGradualTempo(viritura.gradualTempo);\n }\n if (viritura.chordSymbols) {\n measure.chordSymbols = viritura.chordSymbols.map(parseChordSymbol);\n }\n }\n\n return measure;\n}\n\nfunction parseRepeatStart(raw: RawRepeatStart): RepeatStart {\n const r: RepeatStart = {};\n // `times` is an extension on top of repeat-start (schema only requires\n // global-attrs); keep the existing parsing behavior.\n const times = (raw as VendorObj)[\"times\"];\n if (typeof times === \"number\") r.times = times;\n return r;\n}\n\nfunction parseRepeatEnd(raw: RawRepeatEnd): RepeatEnd {\n const r: RepeatEnd = {};\n if (raw.times !== undefined) r.times = raw.times;\n return r;\n}\n\nfunction parseEnding(raw: RawEnding): Ending {\n const ending: Ending = {\n duration: raw.duration,\n numbers: (raw.numbers ?? []) as number[],\n };\n if (raw.open !== undefined) ending.open = raw.open;\n if (raw.color) ending.color = raw.color;\n return ending;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Direction types (tempo, dynamic, ottava, jump markers)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Read `manualOffset` ([dx, dy]) and `avoidCollisions` (bool) from a vendor\n * object onto a target that carries those optional fields. Shared by tempo /\n * dynamic / rehearsal / expression. */\nfunction applyManualPlacement(\n target: { manualOffset?: [number, number]; avoidCollisions?: boolean },\n src: VendorObj | undefined,\n): void {\n if (!src) return;\n const mo = src[\"manualOffset\"];\n if (Array.isArray(mo) && mo.length === 2) {\n const [dx, dy] = mo;\n if (typeof dx === \"number\" && typeof dy === \"number\") target.manualOffset = [dx, dy];\n }\n if (typeof src[\"avoidCollisions\"] === \"boolean\") target.avoidCollisions = src[\"avoidCollisions\"];\n}\n\nexport function parseRhythmicPosition(raw: RawRhythmicPosition): RhythmicPosition {\n // Schema types fraction as integer-unsigned[]; decoded model wants tuple.\n return { fraction: raw.fraction as [number, number] };\n}\n\nfunction parseTempo(raw: RawTempo): Tempo {\n const tempo: Tempo = {\n bpm: raw.bpm,\n value: parseTempoNoteValue(raw.value),\n };\n if (raw.location) {\n tempo.location = parseRhythmicPosition(raw.location);\n }\n // _x.viritura vendor extensions on the tempo object\n // (tempo-extensions is not in the schema yet \u2014 keep narrow casts.)\n const viritura = raw._x?.[\"viritura\"] as VendorObj | undefined;\n if (viritura) {\n if (typeof viritura[\"text\"] === \"string\") tempo.text = viritura[\"text\"];\n if (typeof viritura[\"showMetronomeMark\"] === \"boolean\")\n tempo.showMetronomeMark = viritura[\"showMetronomeMark\"] as boolean;\n if (typeof viritura[\"showText\"] === \"boolean\") tempo.showText = viritura[\"showText\"] as boolean;\n applyManualPlacement(tempo, viritura);\n }\n return tempo;\n}\n\nfunction parseTempoNoteValue(raw: RawNoteValue): TempoNoteValue {\n const tnv: TempoNoteValue = {\n base: raw.base as NoteValueBase,\n };\n if (raw.dots !== undefined) tnv.dots = raw.dots;\n return tnv;\n}\n\n/**\n * Dynamic-group fields that copy across unchanged when present. Listing them\n * keeps the parser's branch count flat as the spec grows.\n */\nconst DYNAMIC_GROUP_SCALAR_FIELDS = [\n \"value\",\n \"residualValue\",\n \"accentPrefix\",\n \"accentSuffix\",\n \"placement\",\n \"prefix\",\n \"relativeValue\",\n \"staff\",\n \"staffEnd\",\n \"suffix\",\n \"visuallyContinues\",\n \"voice\",\n \"wedgeType\",\n] as const satisfies readonly (keyof DynamicGroupBase & keyof RawDynamicGroup)[];\n\nexport function parseDynamicGroup(raw: RawDynamicGroup): DynamicGroup {\n const common: Omit<DynamicGroupBase, \"type\"> = {\n id: raw.id ?? generateId(),\n position: parseRhythmicPosition(raw.position),\n };\n for (const field of DYNAMIC_GROUP_SCALAR_FIELDS) {\n const value = raw[field];\n if (value !== undefined) Object.assign(common, { [field]: value });\n }\n if (raw.end !== undefined) common.end = parseMeasureRhythmicPosition(raw.end);\n if (raw.glyphs !== undefined) common.glyphs = [...raw.glyphs];\n applyManualPlacement(common, raw._x?.[\"viritura\"] as RawDynamicGroupExt | undefined);\n\n switch (raw.type) {\n case \"immediate\":\n if (raw.value === undefined) throw new Error(`Dynamic group ${common.id} of type immediate requires value`);\n return { ...common, type: \"immediate\", value: raw.value };\n case \"gradual\":\n if (common.end === undefined || raw.wedgeType === undefined) {\n throw new Error(`Dynamic group ${common.id} of type gradual requires end and wedgeType`);\n }\n return { ...common, type: \"gradual\", end: common.end, wedgeType: raw.wedgeType };\n case \"relative\":\n if (raw.relativeValue === undefined) {\n throw new Error(`Dynamic group ${common.id} of type relative requires relativeValue`);\n }\n return { ...common, type: \"relative\", relativeValue: raw.relativeValue };\n case \"accent\":\n if (raw.value === undefined) throw new Error(`Dynamic group ${common.id} of type accent requires value`);\n return { ...common, type: \"accent\", value: raw.value };\n }\n}\n\nfunction parseMeasureRhythmicPosition(raw: RawMeasureRhythmicPosition): MeasureRhythmicPosition {\n return {\n measure: raw.measure,\n position: parseRhythmicPosition(raw.position),\n };\n}\n\nexport function parseOttava(raw: RawOttava): Ottava {\n const ott: Ottava = {\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n value: raw.value,\n };\n if (raw.placement !== undefined) ott.placement = raw.placement;\n if (raw.staff !== undefined) ott.staff = raw.staff;\n if (raw.voice) ott.voice = raw.voice;\n return ott;\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Vendor-extension parsers\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function parsePedal(raw: RawPedal): Pedal {\n const pedal: Pedal = {\n type: raw.type as PedalType,\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n };\n if (raw.style) pedal.style = raw.style as PedalLineStyle;\n if (raw.staff !== undefined) pedal.staff = raw.staff;\n if (raw.voice) pedal.voice = raw.voice;\n return pedal;\n}\n\nfunction parseChordRoot(raw: RawChordRoot): ChordRoot {\n const root: ChordRoot = {\n step: raw.step,\n };\n if (raw.alter !== undefined) root.alter = raw.alter;\n return root;\n}\n\nfunction parseChordSymbol(raw: RawChordSymbol): ChordSymbol {\n const cs: ChordSymbol = {\n position: parseRhythmicPosition(raw.position),\n };\n if (raw.root !== undefined) cs.root = parseChordRoot(raw.root);\n if (raw.rawText !== undefined) cs.rawText = raw.rawText;\n if (raw.quality !== undefined) cs.quality = raw.quality;\n if (raw.kindText !== undefined) cs.kindText = raw.kindText;\n if (raw.bass) cs.bass = parseChordRoot(raw.bass);\n if (raw.extension !== undefined) cs.extension = raw.extension;\n if (raw.textOverride !== undefined) cs.textOverride = raw.textOverride;\n return cs;\n}\n\nexport function parseTextExpression(raw: RawTextExpression): TextExpression {\n const expr: TextExpression = {\n text: raw.text,\n position: parseRhythmicPosition(raw.position),\n };\n if (raw.placement) expr.placement = raw.placement as ExpressionPlacement;\n if (raw.staff !== undefined) expr.staff = raw.staff;\n if (raw.voice) expr.voice = raw.voice;\n const mo = raw.manualOffset;\n if (Array.isArray(mo) && mo.length === 2) {\n const [dx, dy] = mo;\n if (typeof dx === \"number\" && typeof dy === \"number\") expr.manualOffset = [dx, dy];\n }\n if (typeof raw.avoidCollisions === \"boolean\") expr.avoidCollisions = raw.avoidCollisions;\n return expr;\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schema direction markers (segno / fine / jump / coda)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parseSegno(raw: RawSegno): Segno {\n const segno: Segno = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.glyph) segno.glyph = raw.glyph;\n if (raw.color) segno.color = raw.color;\n return segno;\n}\n\nfunction parseFine(raw: RawFine): Fine {\n const fine: Fine = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.color) fine.color = raw.color;\n return fine;\n}\n\nfunction parseJump(raw: RawJump | RawVendorJump): Jump {\n return {\n type: raw.type as \"segno\" | \"dsalfine\" | \"dsalcoda\" | \"dcalcoda\",\n location: parseRhythmicPosition(raw.location),\n };\n}\n\n/** Coda is a Viritura vendor extension (no MNX schema). */\nfunction parseCoda(raw: RawCoda): Coda {\n const coda: Coda = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.glyph) coda.glyph = raw.glyph;\n if (raw.color) coda.color = raw.color;\n return coda;\n}\n\n/** Gradual tempo (rit./accel.) is a Viritura vendor extension (no MNX schema). */\nfunction parseGradualTempo(raw: RawGradualTempo): GradualTempo {\n const gt: GradualTempo = {\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n endBpm: raw.endBpm,\n };\n if (raw.startBpm !== undefined) gt.startBpm = raw.startBpm;\n if (raw.kind) gt.kind = raw.kind as GradualTempoKind;\n return gt;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Simple types\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseTimeSignature(raw: RawTime): TimeSignature {\n const ts: TimeSignature = {\n count: raw.count ?? 4,\n unit: raw.unit ?? 4,\n };\n if (raw.display === \"common\" || raw.display === \"cut\") {\n ts.display = raw.display;\n }\n const viritura = raw._x?.[\"viritura\"] as RawTimeExtensions | undefined;\n if (viritura?.beatStructure) ts.beatStructure = [...viritura.beatStructure];\n if (viritura?.groupingDisplay) ts.groupingDisplay = viritura.groupingDisplay;\n if (viritura?.display === \"note\") ts.display = \"note\";\n return ts;\n}\n\nfunction parseKeySignature(raw: RawKey): KeySignature {\n const key: KeySignature = {\n fifths: raw.fifths ?? 0,\n };\n if (raw.color) key.color = raw.color;\n const viritura = raw._x?.[\"viritura\"] as VendorObj | undefined;\n if (viritura?.[\"atonal\"]) key.atonal = true;\n return key;\n}\n\nfunction parseBarline(raw: RawBarline): Barline {\n return {\n type: (raw.type as BarlineType) ?? \"regular\",\n };\n}\n", "// MNX Parser \u2014 parseParts (split from parser.ts)\n//\n// Schema-defined inputs (Part, PartMeasure, Kit, KitComponent, Beam,\n// Arpeggio, NonArpeggio, PositionedClef) now consume the generated\n// Raw* types. The vendor-extension blocks under `_x.viritura` (pedal,\n// pedal, chordSymbol, textExpression, condensingOverride, transposition,\n// part identity) are still narrowed with `as Record<string, unknown>`\n// because the schema deliberately leaves vendor dicts opaque.\n\nimport type {\n PartMeasure,\n Beam,\n PartMeasureArpeggio,\n NonArpeggio,\n MeasureRepeat,\n MeasureRepeatCounter,\n StaffMeter,\n StaffMeterChange,\n} from \"@viritura/core\";\nimport type { Part } from \"@viritura/core\";\nimport type { PositionedClef, Clef } from \"@viritura/core\";\nimport type { ClefSign, KitComponent, NoteheadShape } from \"@viritura/core\";\n\nimport type {\n Part as RawPart,\n PartMeasure as RawPartMeasure,\n Kit as RawKit,\n KitComponent as RawKitComponent,\n Beam as RawBeam,\n Arpeggio as RawArpeggio,\n NonArpeggio as RawNonArpeggio,\n MeasureRepeat as RawMeasureRepeat,\n PositionedClef as RawPositionedClef,\n PositionedStaffConfig as RawPositionedStaffConfig,\n StaffConfig as RawStaffConfig,\n IdPair as RawIdPair,\n} from \"@viritura/core/raw\";\nimport type {\n PartExtensions as RawPartExt,\n PartMeasureExtensions as RawPartMeasureExt,\n KitComponentExtensions as RawKitComponentExt,\n StaffMeterChange as RawStaffMeterChange,\n} from \"@viritura/core/raw-viritura\";\n\nimport { parseDynamicGroup, parseOttava, parseTextExpression, parsePedal, parseRhythmicPosition } from \"./parseGlobal\";\nimport { parseSequences } from \"./parseContent\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Parts\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseParts(arr: RawPart[] | undefined): Part[] {\n if (!arr) return [];\n return arr.map(parsePart);\n}\n\nfunction parsePart(raw: RawPart): Part {\n const part: Part = {\n name: raw.name ?? \"\",\n measures: parsePartMeasures(raw.measures),\n };\n if (raw.id) part.id = raw.id;\n if (raw.shortName !== undefined) part.shortName = raw.shortName;\n if (raw.staves !== undefined) part.staves = raw.staves;\n if (raw.transposition) {\n const t = raw.transposition;\n part.transposition = {\n interval: {\n halfSteps: t.interval.halfSteps,\n staffDistance: t.interval.staffDistance,\n },\n };\n if (t.keyFifthsFlipAt !== undefined) part.transposition.keyFifthsFlipAt = t.keyFifthsFlipAt;\n if (t.prefersWrittenPitches !== undefined) part.transposition.prefersWrittenPitches = t.prefersWrittenPitches;\n }\n if (raw.kit) {\n part.kit = parseKit(raw.kit);\n }\n // _x.viritura vendor extensions (instrument identity + spatial placement)\n const viritura = raw._x?.[\"viritura\"] as RawPartExt | undefined;\n if (viritura) {\n if (viritura.chordSymbolVisibility !== undefined) {\n part.chordSymbolVisibility = viritura.chordSymbolVisibility;\n }\n const ext: { instrumentId?: string; midiProgram?: number; family?: string; spatial?: { x: number; y: number } } =\n {};\n if (typeof viritura.instrumentId === \"string\") ext.instrumentId = viritura.instrumentId;\n if (typeof viritura.midiProgram === \"number\") ext.midiProgram = viritura.midiProgram;\n if (typeof viritura.family === \"string\") ext.family = viritura.family;\n const sp = viritura.spatial;\n if (sp && typeof sp.x === \"number\" && typeof sp.y === \"number\") {\n ext.spatial = { x: sp.x, y: sp.y };\n }\n if (Object.keys(ext).length > 0) {\n part._x = { viritura: ext };\n }\n }\n return part;\n}\n\nfunction parseKit(raw: RawKit): Record<string, KitComponent> {\n const result: Record<string, KitComponent> = {};\n for (const [id, value] of Object.entries(raw)) {\n result[id] = parseKitComponent(value);\n }\n return result;\n}\n\nfunction parseKitComponent(raw: RawKitComponent): KitComponent {\n const c: KitComponent = {\n staffPosition: raw.staffPosition ?? 0,\n };\n if (typeof raw.name === \"string\") c.name = raw.name;\n if (typeof raw.sound === \"string\") c.sound = raw.sound;\n // _x.viritura vendor extensions: notehead (MNX issue #249) + drumKit override.\n const viritura = raw._x?.[\"viritura\"] as RawKitComponentExt | undefined;\n if (viritura?.notehead) {\n c.notehead = viritura.notehead as NoteheadShape;\n }\n if (typeof viritura?.drumKit === \"number\") {\n c.drumKit = viritura.drumKit;\n }\n return c;\n}\n\nfunction parsePartMeasures(arr: RawPartMeasure[] | undefined): PartMeasure[] {\n if (!arr) return [];\n return arr.map(parsePartMeasure);\n}\n\nfunction parsePartMeasure(raw: RawPartMeasure): PartMeasure {\n const pm: PartMeasure = {\n sequences: parseSequences(raw.sequences),\n };\n const clefs = parseClefs(raw.clefs);\n if (clefs !== undefined) pm.clefs = clefs;\n const staffConfigs = parseStaffConfigs(raw.staffConfigs);\n if (staffConfigs !== undefined) pm.staffConfigs = staffConfigs;\n if (raw.beams) {\n pm.beams = raw.beams.map(parseBeam);\n }\n if (raw.arpeggios) {\n pm.arpeggios = raw.arpeggios.map(parseArpeggio);\n }\n if (raw.nonArpeggios) {\n pm.nonArpeggios = raw.nonArpeggios.map(parseNonArpeggio);\n }\n if (raw.dynamics) {\n pm.dynamics = raw.dynamics.map(parseDynamicGroup);\n }\n if (raw.ottavas) {\n pm.ottavas = raw.ottavas.map(parseOttava);\n }\n if (raw.measureRepeat) {\n pm.measureRepeat = parseMeasureRepeat(raw.measureRepeat);\n }\n\n // _x.viritura vendor extensions (none of these are in the MNX schema)\n const viritura = raw._x?.[\"viritura\"] as RawPartMeasureExt | undefined;\n if (viritura) {\n if (viritura.pedals) {\n pm.pedals = viritura.pedals.map(parsePedal);\n }\n if (viritura.expressions) {\n pm.expressions = viritura.expressions.map(parseTextExpression);\n }\n if (viritura.condensingOverride) {\n pm.condensingOverride = viritura.condensingOverride;\n }\n if (viritura.groupingDisplayOverrides && viritura.groupingDisplayOverrides.length > 0) {\n pm.groupingDisplayOverrides = viritura.groupingDisplayOverrides.map((override) => ({\n staff: override.staff,\n groupingDisplay: override.groupingDisplay,\n }));\n }\n if (viritura.staffMeters && viritura.staffMeters.length > 0) {\n pm.staffMeters = viritura.staffMeters.map(parseStaffMeterChange);\n }\n }\n return pm;\n}\n\nfunction parseStaffMeterChange(raw: RawStaffMeterChange): StaffMeterChange {\n if (\"useGlobal\" in raw) {\n return { staff: raw.staff, useGlobal: true };\n }\n const meter: StaffMeter = { count: raw.meter.count, unit: raw.meter.unit };\n if (raw.meter.beatStructure && raw.meter.beatStructure.length > 0) {\n meter.beatStructure = [...raw.meter.beatStructure];\n }\n return { staff: raw.staff, meter, synchronization: raw.synchronization };\n}\n\nfunction parseMeasureRepeat(raw: RawMeasureRepeat): MeasureRepeat {\n const measureRepeat: MeasureRepeat = { number: raw.number };\n if (raw.counter !== undefined) {\n const counter: MeasureRepeatCounter = { count: raw.counter.count };\n if (raw.counter.placement !== undefined) counter.placement = raw.counter.placement;\n measureRepeat.counter = counter;\n }\n if (raw.displayNumber !== undefined) measureRepeat.displayNumber = raw.displayNumber;\n if (raw.staffPosition !== undefined) measureRepeat.staffPosition = raw.staffPosition;\n return measureRepeat;\n}\n\nfunction parseArpeggio(raw: RawArpeggio): PartMeasureArpeggio {\n const arpeggio: PartMeasureArpeggio = {\n position: parseRhythmicPosition(raw.position),\n span: parseIdPair(raw.span),\n };\n if (raw.direction !== undefined) arpeggio.direction = raw.direction;\n if (raw.arrow !== undefined) arpeggio.arrow = raw.arrow;\n if (raw.id !== undefined) arpeggio.id = raw.id;\n return arpeggio;\n}\n\nfunction parseNonArpeggio(raw: RawNonArpeggio): NonArpeggio {\n const nonArpeggio: NonArpeggio = {\n position: parseRhythmicPosition(raw.position),\n span: parseIdPair(raw.span),\n };\n if (raw.id !== undefined) nonArpeggio.id = raw.id;\n return nonArpeggio;\n}\n\nfunction parseIdPair(raw: RawIdPair): { start: string; end: string } {\n return {\n start: raw.start,\n end: raw.end,\n };\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Beams\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseBeam(raw: RawBeam): Beam {\n const beam: Beam = {\n events: raw.events,\n };\n if (raw.beams && raw.beams.length > 0) {\n // Inner beams are `beam-list` (recursive) \u2014 same shape as outer beams.\n beam.beams = raw.beams.map((b) => parseBeam(b as RawBeam));\n }\n if (raw.direction) {\n beam.direction = raw.direction;\n }\n return beam;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Clefs\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseClefs(arr: RawPositionedClef[] | undefined): PositionedClef[] | undefined {\n if (!arr || arr.length === 0) return undefined;\n return arr.map(parsePositionedClef);\n}\n\nfunction parsePositionedClef(raw: RawPositionedClef): PositionedClef {\n const rawClef = raw.clef;\n const clef: Clef = {\n sign: (rawClef.sign as ClefSign) ?? \"G\",\n staffPosition: rawClef.staffPosition ?? -2,\n };\n if (rawClef.color) clef.color = rawClef.color;\n if (rawClef.glyph) clef.glyph = rawClef.glyph;\n if (rawClef.octave !== undefined) clef.octave = rawClef.octave;\n if (rawClef.showOctave !== undefined) clef.showOctave = rawClef.showOctave;\n if (rawClef.hide !== undefined) clef.hide = rawClef.hide;\n\n const result: PositionedClef = { clef };\n\n if (raw.position) {\n if (raw.position.fraction) {\n result.position = { fraction: raw.position.fraction as [number, number] };\n }\n }\n\n if (raw.staff !== undefined) {\n result.staff = raw.staff;\n }\n\n return result;\n}\n\nfunction parseStaffConfigs(arr: RawPositionedStaffConfig[] | undefined): PartMeasure[\"staffConfigs\"] | undefined {\n if (!arr || arr.length === 0) return undefined;\n return arr.map(parsePositionedStaffConfig);\n}\n\nfunction parsePositionedStaffConfig(raw: RawPositionedStaffConfig): NonNullable<PartMeasure[\"staffConfigs\"]>[number] {\n const config: RawStaffConfig = { ...raw.config };\n const positioned: NonNullable<PartMeasure[\"staffConfigs\"]>[number] = { config };\n if (raw.id !== undefined) positioned.id = raw.id;\n if (raw._c !== undefined) positioned._c = raw._c;\n if (raw._x !== undefined) positioned._x = raw._x;\n if (raw.position) positioned.position = parseRhythmicPosition(raw.position);\n if (raw.staff !== undefined) positioned.staff = raw.staff;\n return positioned;\n}\n", "// MNX Parser \u2014 parseContent (split from parser.ts)\n//\n// All schema-defined inputs (sequence, sequence-content, event, grace,\n// tuplet, multi-note-tremolo, space, note, kit-note, tie, pitch,\n// note-value, lyrics, slur, event-markings, rest, written, accidental-\n// display) now consume the generated Raw* types. Vendor-extension\n// blocks under `_x.viritura` (glissandos, slur shape overrides, trill,\n// ornaments, arpeggio direction, fingerings, staccatissimo wedge) keep\n// narrow VendorObj casts because the MNX schema deliberately leaves vendor\n// dicts opaque. Caesura is native as of MNX schema version 36; the legacy\n// `_x.viritura.caesura` vendor form is no longer read (MNX has not yet\n// declared a stable version, so pre-v36 documents are not supported).\n\nimport type {\n NoteEvent,\n Note,\n KitNote,\n Sequence,\n Duration,\n SequenceContent,\n Tie,\n Markings,\n BreathMarkSymbol,\n Fermata,\n FermataSymbol,\n FermataDuration,\n Placement,\n DirectionHint,\n UpDownAuto,\n UpDown,\n OrnamentType,\n Trill,\n Slur,\n Glissando,\n GlissandoStyle,\n ArpeggioDirection,\n CaesuraStyle,\n Lyrics,\n LyricLine,\n LyricLineType,\n Grace,\n GraceType,\n TupletDuration,\n Tuplet,\n TupletBracket,\n TupletDisplaySetting,\n TupletSpan,\n MultiNoteTremolo,\n Space,\n AccidentalDisplay,\n AccidentalEnclosureSymbol,\n Pitch,\n} from \"@viritura/core\";\nimport type { NoteValueBase, StemDirection, Step, Octave } from \"@viritura/core\";\n\nimport type {\n Sequence as RawSequence,\n SequenceContent as RawSequenceContentList,\n Event as RawEvent,\n Grace as RawGrace,\n Tuplet as RawTuplet,\n Space as RawSpace,\n MultiNoteTremolo as RawMultiNoteTremolo,\n Note as RawNote,\n KitNote as RawKitNote,\n Tie as RawTie,\n Pitch as RawPitch,\n NoteValue as RawNoteValue,\n Slur as RawSlur,\n Lyrics as RawLyrics,\n EventMarkings as RawEventMarkings,\n Rest as RawRest,\n} from \"@viritura/core/raw\";\nimport type {\n EventExtensions as RawEventExt,\n EventMarkingsExtensions as RawEventMarkingsExt,\n SlurExtensions as RawSlurExt,\n NoteExtensions as RawNoteExt,\n TupletExtensions as RawTupletExt,\n Glissando as RawGlissando,\n} from \"@viritura/core/raw-viritura\";\n\n/** A single sequence-content item: discriminated union from the schema. */\ntype RawSequenceItem = RawSequenceContentList[number];\n\n/** Shape of a tuplet's `inner`/`outer` (note-value-quantity). */\ntype RawNoteValueQuantity = RawTuplet[\"inner\"];\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sequences (voices)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseSequences(arr: RawSequence[] | undefined): Sequence[] {\n if (!arr) return [];\n return arr.map(parseSequence);\n}\n\nfunction parseSequence(raw: RawSequence): Sequence {\n const seq: Sequence = {\n content: parseContent(raw.content),\n };\n if (raw.fullMeasure) {\n const fm = raw.fullMeasure;\n seq.fullMeasure = {\n visualDuration: fm.visualDuration ? parseDuration(fm.visualDuration) : { base: \"whole\" },\n };\n if (fm.staffPosition !== undefined) {\n seq.fullMeasure.staffPosition = fm.staffPosition;\n }\n if (fm.fermata) seq.fullMeasure.fermata = { ...fm.fermata };\n }\n if (raw.staff !== undefined) seq.staff = raw.staff;\n if (raw.voice !== undefined) seq.voice = raw.voice;\n if (raw.directionHint !== undefined) seq.directionHint = raw.directionHint as DirectionHint;\n return seq;\n}\n\nfunction parseContent(arr: RawSequenceContentList | undefined): SequenceContent[] {\n if (!arr) return [];\n return arr.map(parseSequenceContent);\n}\n\nfunction parseSequenceContent(raw: RawSequenceItem): SequenceContent {\n // The discriminator is `type` \u2014 required on every variant except `event`,\n // where it's optional (default \"event\").\n const type = (raw as { type?: string }).type;\n switch (type) {\n case \"tuplet\":\n return parseTuplet(raw as RawTuplet);\n case \"tremolo\":\n return parseMultiNoteTremolo(raw as RawMultiNoteTremolo);\n case \"grace\":\n return parseGrace(raw as RawGrace);\n case \"space\":\n return parseSpace(raw as RawSpace);\n default:\n return parseEvent(raw as RawEvent);\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Tuplet\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseTupletDuration(raw: RawNoteValueQuantity): TupletDuration {\n return {\n duration: parseDuration(raw.duration),\n multiple: raw.multiple,\n };\n}\n\nfunction parseTuplet(raw: RawTuplet): Tuplet {\n const t: Tuplet = {\n type: \"tuplet\",\n inner: parseTupletDuration(raw.inner),\n outer: parseTupletDuration(raw.outer),\n content: parseContent(raw.content),\n };\n if (raw.bracket !== undefined) {\n t.bracket = raw.bracket as TupletBracket;\n }\n if (raw.showNumber !== undefined) {\n t.showNumber = raw.showNumber as TupletDisplaySetting;\n }\n if (raw.showValue !== undefined) {\n t.showValue = raw.showValue as TupletDisplaySetting;\n }\n if (raw.placement !== undefined) t.placement = raw.placement as Placement;\n const span = (raw._x?.[\"viritura\"] as RawTupletExt | undefined)?.span;\n if (span) t.span = { id: span.id, type: span.type as TupletSpan[\"type\"] };\n return t;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Multi-note tremolo\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseMultiNoteTremolo(raw: RawMultiNoteTremolo): MultiNoteTremolo {\n const tremolo: MultiNoteTremolo = {\n type: \"tremolo\",\n content: raw.content.map(parseEvent),\n marks: raw.marks,\n outer: parseTupletDuration(raw.outer),\n };\n if (raw.individualDuration !== undefined) tremolo.individualDuration = parseDuration(raw.individualDuration);\n return tremolo;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Grace notes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseGrace(raw: RawGrace): Grace {\n const grace: Grace = {\n type: \"grace\",\n content: raw.content.map(parseEvent),\n };\n if (raw.graceType !== undefined) grace.graceType = raw.graceType as GraceType;\n if (raw.slash !== undefined) grace.slash = raw.slash;\n if (raw.color) grace.color = raw.color;\n return grace;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Space\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseSpace(raw: RawSpace): Space {\n return {\n type: \"space\",\n duration: raw.duration as [number, number],\n };\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Events\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseEvent(raw: RawEvent): NoteEvent {\n const event: NoteEvent = {\n type: \"event\",\n duration: parseDuration(raw.duration),\n };\n\n if (raw.id) event.id = raw.id;\n\n if (typeof raw.staff === \"number\") event.staff = raw.staff;\n\n if (raw.rest !== undefined) {\n const restRaw: RawRest = raw.rest;\n const rest: { staffPosition?: number } = {};\n if (typeof restRaw.staffPosition === \"number\") {\n rest.staffPosition = restRaw.staffPosition;\n }\n event.rest = rest;\n }\n\n if (raw.notes) {\n event.notes = parseNotes(raw.notes);\n }\n\n if (raw.kitNotes) {\n event.kitNotes = raw.kitNotes.map(parseKitNote);\n }\n\n if (raw.slurs) {\n event.slurs = raw.slurs.map(parseSlur);\n }\n\n if (raw.markings) {\n event.markings = parseMarkings(raw.markings);\n }\n\n // Native MNX fermata (event-level since v15).\n if (raw.fermata !== undefined) {\n const f = raw.fermata;\n const fermata: Fermata = {};\n if (f.symbol) fermata.symbol = f.symbol as FermataSymbol;\n if (f.duration) fermata.duration = f.duration as FermataDuration;\n if (f.placement) fermata.placement = f.placement as Placement;\n if (f.pointing) fermata.pointing = f.pointing;\n event.fermata = fermata;\n }\n\n if (raw.lyrics) {\n event.lyrics = parseLyrics(raw.lyrics);\n }\n\n if (raw.stemDirection) {\n event.stemDirection = raw.stemDirection as StemDirection;\n }\n\n // _x.viritura vendor extensions on event\n const viritura = raw._x?.[\"viritura\"] as RawEventExt | undefined;\n if (viritura?.glissandos) {\n event.glissandos = viritura.glissandos.map(parseGlissando);\n }\n\n return event;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Markings\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Helper: parse the `placement` field if present.\nfunction parsePlacement(o: { placement?: Placement } | undefined): { placement?: Placement } {\n if (o && o.placement) return { placement: o.placement };\n return {};\n}\n\n// Parse the standard MNX-spec markings on a marking container.\nfunction parseStandardMarkings(raw: RawEventMarkings, m: Markings): void {\n if (raw.staccato !== undefined) m.staccato = parsePlacement(raw.staccato);\n if (raw.staccatissimo !== undefined) m.staccatissimo = parsePlacement(raw.staccatissimo);\n if (raw.spiccato !== undefined) m.spiccato = parsePlacement(raw.spiccato);\n if (raw.tenuto !== undefined) m.tenuto = parsePlacement(raw.tenuto);\n if (raw.accent !== undefined) {\n // MNX spec: accent has only `placement` \u2014 no `pointing`.\n m.accent = parsePlacement(raw.accent);\n }\n if (raw.strongAccent !== undefined) {\n const sa = raw.strongAccent;\n m.strongAccent = parsePlacement(sa);\n if (sa.pointing) m.strongAccent.pointing = sa.pointing as UpDownAuto;\n }\n if (raw.tremolo !== undefined) {\n const t = raw.tremolo;\n m.tremolo = { marks: t.marks, ...parsePlacement(t) };\n }\n if (raw.softAccent !== undefined) m.softAccent = parsePlacement(raw.softAccent);\n if (raw.stress !== undefined) m.stress = parsePlacement(raw.stress);\n if (raw.unstress !== undefined) m.unstress = parsePlacement(raw.unstress);\n if (raw.breath !== undefined) {\n const b = raw.breath;\n m.breath = parsePlacement(b);\n if (b.symbol) m.breath.symbol = b.symbol as BreathMarkSymbol;\n }\n if (raw.caesura !== undefined) {\n m.caesura = {};\n if (raw.caesura.shape) m.caesura.style = raw.caesura.shape as CaesuraStyle;\n if (raw.caesura.marks) m.caesura.marks = raw.caesura.marks as 1 | 2;\n }\n if (raw.bowDirection !== undefined) {\n const bd = raw.bowDirection;\n m.bowDirection = {\n direction: bd.direction as UpDown,\n ...parsePlacement(bd),\n };\n }\n}\n\n// Parse the `_x.viritura` vendor extensions on a marking container.\n// NOTE: schema does NOT define `arpeggio` on event-markings-extensions, but\n// existing fixtures use it (legacy shape). Read it through a narrow cast.\nfunction parseVirituraMarkings(viritura: RawEventMarkingsExt, m: Markings): void {\n if (viritura.staccatissimoWedge !== undefined) {\n m.staccatissimoWedge = parsePlacement(viritura.staccatissimoWedge);\n }\n if (viritura.trill !== undefined) {\n const t = viritura.trill;\n const trill: Trill = {};\n if (t.accidental !== undefined) trill.accidental = t.accidental;\n if (t.showSymbol !== undefined) trill.showSymbol = t.showSymbol;\n if (t.extension !== undefined) {\n trill.extension = {\n target: t.extension.target,\n ...(t.extension.targetEdge !== undefined ? { targetEdge: t.extension.targetEdge } : {}),\n };\n }\n m.trill = trill;\n }\n if (viritura.ornaments !== undefined) {\n m.ornaments = viritura.ornaments.map((o) => o as OrnamentType);\n }\n const arpeggio = (viritura as { arpeggio?: { direction?: string } }).arpeggio;\n if (arpeggio !== undefined) {\n m.arpeggio = {};\n if (arpeggio.direction) m.arpeggio.direction = arpeggio.direction as ArpeggioDirection;\n }\n if (viritura.fingerings !== undefined) {\n m.fingerings = viritura.fingerings.map((f) => ({ finger: f.finger }));\n }\n}\n\nfunction parseMarkings(raw: RawEventMarkings): Markings {\n const m: Markings = {};\n parseStandardMarkings(raw, m);\n const viritura = raw._x?.[\"viritura\"] as RawEventMarkingsExt | undefined;\n if (viritura) parseVirituraMarkings(viritura, m);\n return m;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Lyrics\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseLyrics(raw: RawLyrics): Lyrics {\n const lyrics: Lyrics = {};\n if (raw.lines) {\n const linesObj = raw.lines;\n const lines: Record<string, LyricLine> = {};\n for (const key of Object.keys(linesObj)) {\n const lineRaw = linesObj[key]!;\n const line: LyricLine = { text: lineRaw.text };\n if (lineRaw.type !== undefined) line.type = lineRaw.type as LyricLineType;\n lines[key] = line;\n }\n lyrics.lines = lines;\n }\n return lyrics;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Slurs\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseSlur(raw: RawSlur): Slur {\n const slur: Slur = { target: raw.target };\n if (raw.side) slur.side = raw.side;\n if (raw.sideEnd) slur.sideEnd = raw.sideEnd;\n if (raw.lineType) slur.lineType = raw.lineType as \"solid\" | \"dashed\" | \"dotted\";\n if (raw.startNote) slur.startNote = raw.startNote;\n if (raw.endNote) slur.endNote = raw.endNote;\n // _x.viritura.shape vendor extension: per-handle bezier overrides for engrave mode.\n const viritura = raw._x?.[\"viritura\"] as RawSlurExt | undefined;\n const shapeObj = viritura?.shape;\n if (shapeObj) {\n const shape: NonNullable<Slur[\"shape\"]> = {};\n const readPair = (v: number[] | undefined): [number, number] | undefined => {\n if (Array.isArray(v) && v.length === 2 && typeof v[0] === \"number\" && typeof v[1] === \"number\") {\n return [v[0], v[1]];\n }\n return undefined;\n };\n const p0 = readPair(shapeObj.p0);\n if (p0) shape.p0 = p0;\n const p1 = readPair(shapeObj.p1);\n if (p1) shape.p1 = p1;\n const p2 = readPair(shapeObj.p2);\n if (p2) shape.p2 = p2;\n const p3 = readPair(shapeObj.p3);\n if (p3) shape.p3 = p3;\n if (shape.p0 || shape.p1 || shape.p2 || shape.p3) slur.shape = shape;\n }\n return slur;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Glissandos (vendor extension \u2014 no schema)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseGlissando(raw: RawGlissando): Glissando {\n const gliss: Glissando = { target: raw.target };\n if (raw.kind) gliss.kind = raw.kind;\n if (raw.style) gliss.style = raw.style as GlissandoStyle;\n if (raw.text) gliss.text = raw.text;\n if (raw.showText !== undefined) gliss.showText = raw.showText;\n return gliss;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Notes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseNotes(arr: RawNote[]): Note[] {\n return arr.map(parseNote);\n}\n\nfunction parseNote(raw: RawNote): Note {\n const note: Note = {\n pitch: parsePitch(raw.pitch),\n };\n\n if (raw.id) note.id = raw.id;\n\n if (raw.ties) {\n note.ties = raw.ties.map(parseTie);\n }\n\n if (typeof raw.staff === \"number\") note.staff = raw.staff;\n\n if (raw.accidentalDisplay) {\n const ad = raw.accidentalDisplay;\n const display: AccidentalDisplay = { show: ad.show };\n if (ad.force !== undefined) display.force = ad.force;\n if (ad.enclosure) {\n display.enclosure = { symbol: ad.enclosure.symbol as AccidentalEnclosureSymbol };\n }\n note.accidentalDisplay = display;\n }\n\n if (raw.written) {\n note.written = {};\n if (raw.written.diatonicDelta !== undefined) note.written.diatonicDelta = raw.written.diatonicDelta;\n }\n\n // _x.viritura.notehead: per-note notehead-shape override (MNX issue #249).\n const viritura = raw._x?.[\"viritura\"] as RawNoteExt | undefined;\n if (viritura?.notehead) note.notehead = viritura.notehead;\n\n return note;\n}\n\nfunction parseKitNote(raw: RawKitNote): KitNote {\n const kn: KitNote = {\n kitComponent: raw.kitComponent,\n };\n if (raw.id) kn.id = raw.id;\n if (typeof raw.staff === \"number\") kn.staff = raw.staff;\n if (raw.ties) {\n kn.ties = raw.ties.map(parseTie);\n }\n if (raw.perform !== undefined) {\n const p = raw.perform;\n const perform: { id?: string } = {};\n if (typeof p.id === \"string\") perform.id = p.id;\n kn.perform = perform;\n }\n return kn;\n}\n\nfunction parseTie(raw: RawTie): Tie {\n const tie: Tie = {};\n if (raw.target) tie.target = raw.target;\n if (raw.targetType) tie.targetType = raw.targetType;\n if (raw.side) tie.side = raw.side;\n if (raw.lv !== undefined) tie.lv = raw.lv;\n return tie;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Pitch\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parsePitch(raw: RawPitch): Pitch {\n const pitch: Pitch = {\n step: (raw.step as Step) ?? \"C\",\n octave: (raw.octave as Octave) ?? 4,\n };\n if (raw.alter !== undefined) pitch.alter = raw.alter;\n return pitch;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration (note-value)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseDuration(raw: RawNoteValue): Duration {\n const dur: Duration = {\n base: (raw.base as NoteValueBase) ?? \"quarter\",\n };\n if (raw.dots !== undefined) dur.dots = raw.dots;\n return dur;\n}\n", "// MNX Parser \u2014 parseLayout (split from parser.ts)\n//\n// Consumes the generated Raw* wire types from @viritura/core/raw and produces\n// the decoded model from @viritura/core. Field access is now type-checked\n// against the MNX schema; schema drift surfaces as compile errors here.\n//\n// Vendor-extension access (`_x.viritura.*`) still requires casts because\n// the schema deliberately leaves vendor dicts untyped \u2014 see\n// docs/spec/viritura-extensions.md for the typed shapes we layer on top.\n\nimport type {\n LayoutDefinition,\n LayoutContent,\n LayoutGroup,\n LayoutStaff,\n LayoutSource,\n ScoreDefinition,\n PageDefinition,\n SystemDefinition,\n LayoutChange,\n MultimeasureRestRange,\n PageSetup,\n PageTurnSettings,\n InstrumentNameDisplaySettings,\n} from \"@viritura/core\";\n\nimport type {\n SystemLayout as RawSystemLayout,\n StaffGroup as RawStaffGroup,\n Staff as RawStaff,\n StaffSource as RawStaffSource,\n Score as RawScoreDef,\n Page as RawPage,\n System as RawSystem,\n LayoutChange as RawLayoutChange,\n MultimeasureRest as RawMultimeasureRest,\n} from \"@viritura/core/raw\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MNX Layout definitions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseLayoutDefinition(raw: RawSystemLayout): LayoutDefinition {\n const ld: LayoutDefinition = {\n // `id` is optional in the schema (every global-attrs has optional id);\n // assignMissingIds() later fills any gaps. Preserve undefined here.\n id: raw.id as string,\n content: raw.content.map(parseLayoutContent),\n };\n\n // Vendor extension: _x.viritura.derived \u2014 flags auto-derived layouts\n // (e.g. hide-staff prunes) so the GC can distinguish them from\n // user-authored layouts.\n const viritura = raw._x?.[\"viritura\"] as Record<string, unknown> | undefined;\n if (viritura?.[\"derived\"] === true) {\n ld._x = { viritura: { derived: true } };\n }\n\n return ld;\n}\n\nfunction parseLayoutContent(raw: RawStaffGroup | RawStaff): LayoutContent {\n if (raw.type === \"group\") return parseLayoutGroup(raw);\n return parseLayoutStaff(raw);\n}\n\nfunction parseLayoutGroup(raw: RawStaffGroup): LayoutGroup {\n const group: LayoutGroup = {\n type: \"group\",\n content: raw.content.map(parseLayoutContent),\n };\n if (raw.symbol) group.symbol = raw.symbol;\n if (raw.label) group.label = raw.label;\n if (raw.barlineStyle) group.barlineStyle = raw.barlineStyle;\n return group;\n}\n\nfunction parseLayoutStaff(raw: RawStaff): LayoutStaff {\n const staff: LayoutStaff = {\n type: \"staff\",\n sources: (raw.sources ?? []).map(parseLayoutSource),\n };\n if (raw.label) staff.label = raw.label;\n if (raw.labelref) staff.labelref = raw.labelref;\n return staff;\n}\n\nfunction parseLayoutSource(raw: RawStaffSource): LayoutSource {\n const src: LayoutSource = {\n part: raw.part,\n };\n if (raw.staff !== undefined) src.staff = raw.staff;\n if (raw.stem) src.stem = raw.stem;\n if (raw.voice) src.voice = raw.voice;\n if (raw.labelref) src.labelref = raw.labelref;\n return src;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MNX Score definitions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// eslint-disable-next-line complexity -- MNX score definitions flatten several independent optional vendor fields\nexport function parseScoreDefinition(raw: RawScoreDef): ScoreDefinition {\n const sd: ScoreDefinition = {};\n if (raw.name) sd.name = raw.name;\n if (raw.layout) sd.layout = raw.layout;\n if (raw.multimeasureRests && raw.multimeasureRests.length > 0) {\n sd.multimeasureRests = raw.multimeasureRests.map((m: RawMultimeasureRest): MultimeasureRestRange => ({\n start: m.start,\n duration: m.duration,\n }));\n }\n if (raw.pages && raw.pages.length > 0) {\n sd.pages = raw.pages.map(parsePageDefinition);\n }\n if (raw.useWritten !== undefined) sd.useWritten = raw.useWritten;\n\n // Vendor extension: _x.viritura.pageSetup\n // (vendor dicts are untyped in the schema; narrow-cast the known shape.)\n const viritura = raw._x?.[\"viritura\"] as Record<string, unknown> | undefined;\n const labelsRaw = viritura?.[\"instrumentNameDisplay\"] as Record<string, unknown> | undefined;\n if (labelsRaw) {\n sd.instrumentNameDisplay = {\n firstSystem: labelsRaw[\"firstSystem\"] as InstrumentNameDisplaySettings[\"firstSystem\"],\n subsequentSystems: labelsRaw[\"subsequentSystems\"] as InstrumentNameDisplaySettings[\"subsequentSystems\"],\n };\n }\n const layoutBreaksRaw = viritura?.[\"layoutBreaks\"];\n if (Array.isArray(layoutBreaksRaw)) {\n sd.layoutBreaks = layoutBreaksRaw.flatMap((value) => {\n if (!value || typeof value !== \"object\") return [];\n const entry = value as Record<string, unknown>;\n if (typeof entry[\"measure\"] !== \"string\") return [];\n if (entry[\"kind\"] !== \"system\" && entry[\"kind\"] !== \"page\") return [];\n return [{ measure: entry[\"measure\"], kind: entry[\"kind\"] }];\n });\n }\n const psRaw = viritura?.[\"pageSetup\"] as Record<string, unknown> | undefined;\n if (psRaw) {\n const pageSetup: Partial<PageSetup> = {};\n if (psRaw[\"width\"] !== undefined) pageSetup.width = psRaw[\"width\"] as number;\n if (psRaw[\"height\"] !== undefined) pageSetup.height = psRaw[\"height\"] as number;\n if (psRaw[\"orientation\"]) pageSetup.orientation = psRaw[\"orientation\"] as \"portrait\" | \"landscape\";\n if (psRaw[\"margins\"]) {\n const m = psRaw[\"margins\"] as Record<string, unknown>;\n pageSetup.margins = {\n top: (m[\"top\"] as number) ?? 15,\n right: (m[\"right\"] as number) ?? 10,\n bottom: (m[\"bottom\"] as number) ?? 15,\n left: (m[\"left\"] as number) ?? 15,\n };\n }\n if (psRaw[\"spatiumMm\"] !== undefined) pageSetup.spatiumMm = psRaw[\"spatiumMm\"] as number;\n const ptRaw = psRaw[\"pageTurns\"] as Record<string, unknown> | undefined;\n if (ptRaw && ptRaw[\"enabled\"] !== undefined) {\n const weightsRaw = ptRaw[\"weights\"] as Record<string, unknown> | undefined;\n const pageTurns: PageTurnSettings = {\n enabled: ptRaw[\"enabled\"] as boolean,\n ...(ptRaw[\"preset\"] ? { preset: ptRaw[\"preset\"] as \"relaxed\" | \"professional\" } : {}),\n ...(ptRaw[\"comfortableSecs\"] !== undefined ? { comfortableSecs: ptRaw[\"comfortableSecs\"] as number } : {}),\n ...(ptRaw[\"vsSecs\"] !== undefined ? { vsSecs: ptRaw[\"vsSecs\"] as number } : {}),\n ...(ptRaw[\"minAcceptableSecs\"] !== undefined\n ? { minAcceptableSecs: ptRaw[\"minAcceptableSecs\"] as number }\n : {}),\n ...(ptRaw[\"targetFillFraction\"] !== undefined\n ? { targetFillFraction: ptRaw[\"targetFillFraction\"] as number }\n : {}),\n ...(ptRaw[\"minFillFraction\"] !== undefined ? { minFillFraction: ptRaw[\"minFillFraction\"] as number } : {}),\n ...(ptRaw[\"verticalJustifyThreshold\"] !== undefined\n ? { verticalJustifyThreshold: ptRaw[\"verticalJustifyThreshold\"] as number }\n : {}),\n ...(ptRaw[\"allowPartialPages\"] !== undefined\n ? { allowPartialPages: ptRaw[\"allowPartialPages\"] as boolean }\n : {}),\n ...(ptRaw[\"allowIntentionalBlanks\"] !== undefined\n ? { allowIntentionalBlanks: ptRaw[\"allowIntentionalBlanks\"] as boolean }\n : {}),\n ...(ptRaw[\"titlePage\"] ? { titlePage: ptRaw[\"titlePage\"] as PageTurnSettings[\"titlePage\"] } : {}),\n ...(ptRaw[\"firstPageRecto\"] !== undefined ? { firstPageRecto: ptRaw[\"firstPageRecto\"] as boolean } : {}),\n ...(ptRaw[\"emitVsMarks\"] !== undefined ? { emitVsMarks: ptRaw[\"emitVsMarks\"] as boolean } : {}),\n ...(ptRaw[\"defaultBpm\"] !== undefined ? { defaultBpm: ptRaw[\"defaultBpm\"] as number } : {}),\n };\n if (weightsRaw) {\n pageTurns.weights = {\n ...(weightsRaw[\"density\"] !== undefined ? { density: weightsRaw[\"density\"] as number } : {}),\n ...(weightsRaw[\"turn\"] !== undefined ? { turn: weightsRaw[\"turn\"] as number } : {}),\n ...(weightsRaw[\"sparse\"] !== undefined ? { sparse: weightsRaw[\"sparse\"] as number } : {}),\n ...(weightsRaw[\"titlePage\"] !== undefined ? { titlePage: weightsRaw[\"titlePage\"] as number } : {}),\n ...(weightsRaw[\"blankPage\"] !== undefined ? { blankPage: weightsRaw[\"blankPage\"] as number } : {}),\n ...(weightsRaw[\"timeMarking\"] !== undefined ? { timeMarking: weightsRaw[\"timeMarking\"] as number } : {}),\n };\n }\n pageSetup.pageTurns = pageTurns;\n }\n if (Object.keys(pageSetup).length > 0) {\n sd.pageSetup = pageSetup as PageSetup;\n }\n }\n\n return sd;\n}\n\nfunction parsePageDefinition(raw: RawPage): PageDefinition {\n return {\n systems: (raw.systems ?? []).map(parseSystemDefinition),\n };\n}\n\nfunction parseSystemDefinition(raw: RawSystem): SystemDefinition {\n const sd: SystemDefinition = {\n measure: raw.measure,\n };\n if (raw.layout) sd.layout = raw.layout;\n if (raw.layoutChanges && raw.layoutChanges.length > 0) {\n sd.layoutChanges = raw.layoutChanges.map((lc: RawLayoutChange): LayoutChange => ({\n layout: lc.layout,\n location: {\n measure: lc.location.measure,\n ...(lc.location.position\n ? {\n position: {\n // Schema types fraction as integer-unsigned[]; the decoded\n // model uses a [num, den] tuple. Narrow array\u2192tuple here.\n fraction: lc.location.position.fraction as [number, number],\n },\n }\n : {}),\n },\n }));\n }\n return sd;\n}\n", "/**\n * Re-export of @viritura/core diagnostics. Kept here to avoid breaking the\n * `./mnx/diagnostics` import path used by the parser; new code should import\n * from `@viritura/core` (or via `@viritura/format`'s top-level re-export).\n */\nexport { DiagnosticCollector, ptr, ptrChild, type MnxDiagnostic, type DiagnosticSeverity } from \"@viritura/core\";\n", "/**\n * @viritura/midi \u2014 MIDI event timeline and Web MIDI API output.\n *\n * Converts Viritura score models into MIDI event timelines for playback\n * scheduling, and provides Web MIDI API integration for external synths.\n */\n\n// MIDI types\nexport type { MidiTimeline, MidiEvent, TempoMapEntry, TimelineDiagnostic } from \"./types\";\n\n// Timeline generator\nexport { generateTimeline, durationBeats, expandMeasureOrder } from \"./timeline\";\nexport { expandMeasureRepeats, expandScoreMeasureRepeats } from \"./measureRepeats\";\nexport type { TimelineOptions } from \"./timeline\";\nexport { getChordPlaybackPart } from \"./chordPlayback\";\nexport type { ChordPlaybackPart } from \"./chordPlayback\";\n\n// Notation-level performance events for VST articulation mapping\nexport { generatePerformanceEvents } from \"./performanceEvents\";\nexport type { PerformanceEvent, PerformanceNote, Articulations, PlayingState } from \"./performanceEvents\";\n\n// Tempo map\nexport { buildTempoMap, measureBeatsFromTime, tempoNoteBeats, effectiveQpm, spqAtTime, DEFAULT_BPM } from \"./tempoMap\";\n\n// Dynamics\nexport { applyArticulationVelocity, articulationDurationScale, metricAccentOffset, velocityHumanize } from \"./dynamics\";\n\n// Coupled dynamics envelope (velocity \u2194 CC11)\nexport {\n DYNAMIC_AXES,\n DEFAULT_DYNAMIC,\n dynamicToAxes,\n buildDynamicsEnvelope,\n sampleDynamics,\n noteVelocityAt,\n hasAnyDynamics,\n cc11Events,\n} from \"./dynamicsEnvelope\";\nexport type { DynamicAxes, DynamicsEnvelope, Cc11Event } from \"./dynamicsEnvelope\";\nexport {\n compileDynamicProgram,\n laneForScope,\n laneForSequence,\n playbackLaneId,\n realizeDynamicsEnvelope,\n selectDynamicResponseProfile,\n} from \"./dynamicPlayback\";\nexport type {\n DynamicPlaybackDiagnostic,\n DynamicProgram,\n DynamicResponseProfile,\n PlaybackLane,\n PlaybackLaneId,\n} from \"./dynamicPlayback\";\n\n// Web MIDI API\nexport { isWebMidiSupported, listMidiInputs, listMidiOutputs, queryMidiPermission } from \"./webMidi\";\nexport { MidiOutputManager } from \"./MidiOutputManager\";\nexport type { MidiOutputPort, MidiOutputManagerEvents } from \"./MidiOutputManager\";\nexport { MidiInputManager, decodeMidiInputMessage } from \"./midiInput\";\nexport type { MidiInputManagerEvents, MidiInputMessage, MidiInputMessageKind, MidiInputPort } from \"./midiInput\";\nexport {\n controllerPortRole,\n controllerProfileSourceUrl,\n findControllerProfile,\n listControllerProfiles,\n parseControllerProfile,\n resolveControllerAction,\n isControllerPreviewMessage,\n} from \"./controllerProfiles\";\nexport type {\n ControllerAction,\n ControllerBinding,\n ControllerPortProfile,\n ControllerProfile,\n} from \"./controllerProfiles\";\n", "/**\n * MIDI timeline generator \u2014 converts a Score into a time-ordered sequence\n * of MIDI noteOn/noteOff events with absolute timestamps.\n *\n * The generator is stateless and deterministic: the same Score always\n * produces the same MidiTimeline.\n *\n * Algorithm:\n * 1. Expand repeats and jumps into a linear measure order\n * 2. Build a tempo map from the expanded global measures\n * 3. For each part, walk the expanded measures and emit MIDI events\n * 4. Resolve ties (suppress intermediate noteOn/noteOff pairs)\n * 5. Sort all events by time\n */\n\n/* eslint-disable max-lines -- The ornament-expansion cluster (kit roll,\n single/multi-note tremolo, trill, grace, tuplet) is mutually recursive with\n processNoteEvent/processContentItem and shares a dozen module-private\n helpers (eventTime, eventSeconds, timingHumanize, pitchToMidi, \u2026). Splitting\n it out would require exporting those internals and create circular imports \u2014\n more risk than the ~10-line overage warrants. Revisit if the timing/event\n helpers are first promoted to their own sibling. */\n\nimport type {\n Score,\n GlobalMeasure,\n NoteEvent,\n Sequence,\n SequenceContent,\n Duration,\n TimeSignature,\n Grace,\n Note,\n KitNote,\n Pitch,\n Step,\n Octave,\n} from \"@viritura/core\";\nimport { pitchToMidi, isRest, DURATION_BEATS, SHARP_ORDER, FLAT_ORDER } from \"@viritura/core\";\nimport { detectToCodaMeasureIndex, expandMeasureOrder } from \"./repeatExpansion\";\nimport { expandScoreMeasureRepeats } from \"./measureRepeats\";\nimport { compileChordPlayback, chordMidiEvents } from \"./chordPlayback\";\nimport { analyzeImpliedSectionDynamics, type ImpliedSectionDynamicAnchor } from \"./sectionDynamics\";\nimport { buildHoldSchedule, type HoldSchedule, type MeasureHold } from \"./holds\";\nimport { playbackGlobalMeasures, suppressCadenzaFermataHolds } from \"./cadenzaTiming\";\nimport { collectPartLegatoOut, flushVoiceLegato, flushAllLegato, pushVoiceLegato, resolveVoiceLegato } from \"./legato\";\nimport { ARCO_CAPABLE_PROGRAMS, muteFamilyForProgram, applyMeasureTechniques, type TechniqueState } from \"./technique\";\nimport { resolveActiveTime, resolvePartStaffMeterTable, staffMeterRatioAt } from \"./staffMeterTiming\";\nexport { expandMeasureOrder };\n\n/** GM percussion channel (0-based; channel 10 in 1-based MIDI numbering). */\nexport const GM_DRUM_CHANNEL = 9;\n\n/**\n * Resolve the effective MIDI note for a pitched note.\n * For pitched notes, returns pitchToMidi(note.pitch).\n */\nfunction noteToMidi(note: Note): number {\n return pitchToMidi(note.pitch);\n}\n\n/** Resolve the effective MIDI note for a kit-note via the kitMidiMap. */\nfunction kitNoteToMidi(kn: KitNote, kitMidiMap: ReadonlyMap<string, number>): number {\n return kitMidiMap.get(kn.kitComponent) ?? -1;\n}\nimport type { MidiEvent, MidiTimeline, TempoMapEntry, TimelineDiagnostic } from \"./types\";\nimport { buildTempoMap, buildTempoModel } from \"./tempoMap\";\nimport { TempoModel } from \"./tempoModel\";\nimport {\n applyArticulationVelocity,\n articulationDurationScale,\n metricAccentOffset,\n velocityHumanize,\n timingHumanize,\n} from \"./dynamics\";\nimport { noteVelocityAt, emitDynamicsCc11, type DynamicsEnvelope } from \"./dynamicsEnvelope\";\nimport { compileDynamicProgram, laneForScope, laneForSequence } from \"./dynamicPlayback\";\nimport type { DynamicProgram } from \"./dynamicPlayback\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Constants\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Duration per grace note in beats (\u2248 32nd note). */\nconst GRACE_NOTE_BEATS = 0.125;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration helpers\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Compute quarter-note beats for a Duration (base + dots). */\nexport function durationBeats(d: Duration): number {\n const base = DURATION_BEATS[d.base] ?? 1;\n if (!d.dots) return base;\n return base * (2 - Math.pow(2, -d.dots));\n}\n\n/** Convert a measure-relative fraction (of a whole note) to quarter-note beats. */\nexport function fractionToBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Main timeline generator\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Options for timeline generation. */\nexport interface TimelineOptions {\n /** GM program number per part index (used for tremolo sound selection). */\n partPrograms?: number[];\n /** Include the global derived Chords stream (default true). Full-score and\n * current-part extracts include it. Explicit selected-instrument callers\n * must pass false unless the entire visible score is selected. */\n includeGlobalChords?: boolean;\n /** Expand playback repeats and jumps (default true); false plays measures once in score order. */\n expandRepeats?: boolean;\n}\n\n/**\n * Per-part shared context threaded through every process* helper.\n * Pulled out so the process* signatures don't carry 13\u201315 parameters each.\n *\n * `partIndex`/`channel`/`gmProgram`/`timeSig`/`kitMidiMap`/`tieTargets`/\n * `pendingTieOffs`/`out`/`tempoMap` are invariant for the lifetime of a\n * single part's traversal; per-event state (measureStartTime, beatOffset,\n * baseVelocity, tupletRatio) stays as explicit parameters because it\n * mutates per recursion step.\n */\nexport interface PartCtx {\n readonly tempoMap: readonly TempoMapEntry[];\n /** Continuous tempo map (single source of timing). Event times are\n * `model.timeAtBeat(measureStartBeat + beatOffset)`, resolved at sub-bar\n * granularity so mid-bar tempo changes, rit./accel. and fermata/caesura time\n * insertions all take effect at any nesting depth (no per-bar quantization). */\n readonly model: TempoModel;\n /** Global quarter-note beat at the current measure's start (the hold-free\n * beat axis the model integrates over). */\n readonly measureStartBeat: number;\n readonly partIndex: number;\n readonly channel: number;\n readonly tieTargets: ReadonlySet<string>;\n readonly pendingTieOffs: Map<string, MidiEvent>;\n /** Event ids interior to a slur span; their noteOff is deferred and resolved as a legato overlap. */\n readonly legatoOutIds: ReadonlySet<string>;\n /** Deferred legato noteOffs awaiting the next note's onset, bucketed BY VOICE\n * so a slur in one voice isn't resolved by a note in another. Each off carries\n * its natural (fallback) time, overwritten on resolution. Threaded across\n * measures (like `pendingTieOffs`) so slurs cross bar lines. */\n readonly pendingLegato: Map<string, MidiEvent[]>;\n /** Identity of the voice (sequence) currently being processed. Tie and legato\n * pairing state is keyed by this so a multi-voice part (e.g. piano, two hands\n * sharing pitches) doesn't cross-contaminate noteOn/noteOff pairing. Mutated\n * per sequence in processPart. */\n voiceKey: string;\n readonly out: MidiEvent[];\n readonly gmProgram: number;\n /** Meter driving beat semantics (metric accent) for the sequence currently\n * being processed: the global measure's time signature, or a staff-local\n * meter's own time signature when the current sequence's staff carries an\n * effective `_x.viritura.staffMeters` declaration. Mutated per sequence in\n * `processPart`, mirroring `sequenceStaff`. */\n timeSig: TimeSignature;\n /** Exact ratio scaling this staff's written beats into global-measure-\n * equivalent beats for absolute playback timing (`1` for an ordinary staff\n * or a `sharedDuration` staff-local meter; the derived mapping ratio for\n * `fitMeasure`). Applied in `eventTime`/`eventSeconds` so a `fitMeasure`\n * staff's notes land at the same wall-clock position other staves'\n * corresponding beats do, even though its written durations differ.\n * Mutated per sequence, alongside `timeSig`. */\n staffMeterRatio: number;\n /** Active key signature (circle-of-fifths) at this measure; drives trill auxiliaries. */\n readonly keyFifths: number;\n readonly kitMidiMap: ReadonlyMap<string, number> | undefined;\n /** kit-component-id \u2192 GS drum-kit program override (bank 128). Hits on these\n * components play on a separate drum channel loaded with the given kit, so a\n * percussion staff can borrow a sound its main kit lacks (e.g. a Tam-tam from\n * the Ethnic kit). undefined when no component overrides its kit. */\n readonly kitAltProgramMap: ReadonlyMap<string, number> | undefined;\n /** Coupled velocity/CC11 dynamics over time for this part. Velocity is sampled\n * per note onset; CC11 is emitted separately (see emitDynamicsCc11). */\n dynamicsEnvelope: DynamicsEnvelope;\n playbackLaneId: string;\n readonly dynamicProgram: DynamicProgram;\n sequenceStaff: number;\n sequenceVoice: string;\n /** Seconds per quarter-note beat at this measure's start (for fermata resume). */\n readonly measureSpq: number;\n /** Fermata groups in this measure (from the hold schedule). The hold's time\n * insertion is baked into the tempo model at `insertBeat`, so a carrier whose\n * onset falls in [startBeat, spanEndBeat) rings through the gap automatically\n * (its model duration already spans the insertion); we only force-extend a\n * member that ends BEFORE `insertBeat` up to the group's resume time. Beats\n * are measure-relative. Empty when the measure has no fermata. */\n readonly fermataGroups: readonly { startBeat: number; spanEndBeat: number; insertBeat: number }[];\n}\n\n/** An empty timeline (no measures / no playable content). */\nfunction emptyTimeline(): MidiTimeline {\n return {\n events: [],\n duration: 0,\n totalBeats: 0,\n tempoMap: [],\n measureStartTimes: [],\n model: TempoModel.build([]),\n measureStartBeats: [],\n expandedMeasureToOriginal: [],\n measureTimeSignatures: [],\n diagnostics: [],\n };\n}\n\n/**\n * Generate a MIDI timeline from a Score.\n *\n * @param score - The score to convert\n * @param options - Optional generation options\n * @returns A MidiTimeline with time-sorted events, total duration, and tempo map\n */\nexport function generateTimeline(inputScore: Score, options?: TimelineOptions): MidiTimeline {\n // Simile marks stand in for earlier bars; substitute the real music before\n // anything downstream reads part measures.\n const score = expandScoreMeasureRepeats(inputScore);\n const impliedSectionDynamics = analyzeImpliedSectionDynamics(score);\n const globalMeasures = score.global.measures;\n if (globalMeasures.length === 0) {\n return emptyTimeline();\n }\n\n const chords = compileChordPlayback(score, options?.includeGlobalChords);\n // Step 1: Expand repeats/jumps into linear measure order\n const toCodaMeasureIndex = detectToCodaMeasureIndex(score);\n const measureOrder =\n options?.expandRepeats === false\n ? globalMeasures.map((_, index) => index)\n : expandMeasureOrder(globalMeasures, { toCodaMeasureIndex });\n if (measureOrder.length === 0) {\n return emptyTimeline();\n }\n\n // Step 2: Build the fermata/caesura hold schedule, then the tempo map. The\n // schedule shifts every later measure's start time so holds keep all parts\n // aligned through the pause.\n const expandedGlobal = playbackGlobalMeasures(score, measureOrder);\n const holdSchedule = suppressCadenzaFermataHolds(\n expandedGlobal,\n buildHoldSchedule(score, measureOrder, globalMeasures),\n );\n // Continuous tempo model + global beat axis (single source of event timing).\n // `measureStartTimes` is derived FROM the model so it matches event timing\n // through sub-bar tempo changes / holds. The legacy `tempoMap` entry array is\n // kept for UI, dynamics, and the score-engine consumers.\n const { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule);\n const tempoBuild = buildTempoModel(expandedGlobal, holdSchedule);\n const { model, measureStartTimes, measureStartBeats } = tempoBuild;\n const timing: TimelineTiming = { tempoMap, measureStartTimes, model, measureStartBeats };\n\n // Step 3: For each part, generate MIDI events\n const allEvents: MidiEvent[] = [];\n const partChannels = allocatePartChannels(score.parts);\n\n for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {\n processPart(\n score,\n partIdx,\n options,\n partChannels,\n timing,\n measureOrder,\n globalMeasures,\n holdSchedule,\n impliedSectionDynamics.get(partIdx) ?? [],\n allEvents,\n );\n }\n\n if (chords) {\n for (const event of chordMidiEvents(chords, measureOrder, tempoBuild)) allEvents.push(event);\n }\n const diagnostics = assignPhysicalMidiChannels(allEvents, score);\n\n // Step 4: Sort by time, then noteOff before programChange before noteOn at same time\n const EVENT_TYPE_ORDER: Record<string, number> = { noteOff: 0, programChange: 1, controlChange: 1, noteOn: 2 };\n allEvents.sort((a, b) => {\n if (Math.abs(a.time - b.time) > 1e-9) return a.time - b.time;\n const aOrder = EVENT_TYPE_ORDER[a.type] ?? 1;\n const bOrder = EVENT_TYPE_ORDER[b.type] ?? 1;\n if (aOrder !== bOrder) return aOrder - bOrder;\n return a.midiNote - b.midiNote;\n });\n\n const duration = model.timeAtBeat(tempoBuild.totalBeats);\n\n // Active time signature per expanded measure (for the metronome click grid).\n const measureTimeSignatures = measureOrder.map((origIdx) => {\n const ts = resolveActiveTime(globalMeasures, origIdx);\n return { count: ts.count, unit: ts.unit };\n });\n\n return {\n events: allEvents,\n duration,\n tempoMap,\n measureStartTimes,\n model,\n totalBeats: tempoBuild.totalBeats,\n measureStartBeats,\n expandedMeasureToOriginal: measureOrder,\n measureTimeSignatures,\n diagnostics,\n };\n}\n\n/** Assign one MIDI 1 channel per independent dynamic lane where possible.\n * Internal SF2 playback ignores this projection and allocates unbounded synth\n * instances; this mapping is for Web MIDI and future SMF export. */\nfunction assignPhysicalMidiChannels(events: MidiEvent[], score: Score): TimelineDiagnostic[] {\n const melodicChannels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];\n const lanesByPart = new Map<number, Set<string>>();\n for (const event of events) {\n const laneId = event.playbackLaneId ?? `part:${event.partIndex}`;\n const lanes = lanesByPart.get(event.partIndex) ?? new Set<string>();\n lanes.add(laneId);\n lanesByPart.set(event.partIndex, lanes);\n }\n\n const channelByLane = new Map<string, number>();\n const diagnostics: TimelineDiagnostic[] = [];\n let melodicIndex = 0;\n for (const [partIndex, laneSet] of [...lanesByPart].sort(([left], [right]) => left - right)) {\n const laneIds = [...laneSet].sort();\n const percussion = !!score.parts[partIndex]?.kit && Object.keys(score.parts[partIndex]!.kit!).length > 0;\n if (percussion) {\n for (const laneId of laneIds) channelByLane.set(laneId, GM_DRUM_CHANNEL);\n if (laneIds.length > 1) {\n diagnostics.push({\n code: \"percussion-lane-collapse\",\n message: `Physical MIDI collapses ${laneIds.length} scoped percussion lanes in part ${partIndex} onto channel 10.`,\n playbackLaneIds: laneIds,\n });\n }\n continue;\n }\n for (const laneId of laneIds) {\n if (melodicIndex < melodicChannels.length) {\n channelByLane.set(laneId, melodicChannels[melodicIndex++]!);\n } else {\n channelByLane.set(laneId, melodicChannels[melodicIndex++ % melodicChannels.length]!);\n diagnostics.push({\n code: \"midi-channel-capacity\",\n message: `Physical MIDI channel capacity exceeded; lane ${laneId} must share a channel.`,\n playbackLaneIds: [laneId],\n });\n }\n }\n }\n\n for (const event of events) {\n event.channel = channelByLane.get(event.playbackLaneId ?? `part:${event.partIndex}`) ?? event.channel;\n }\n return diagnostics;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Tie target collection\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Add every tie-target note id from `notes` to `targets`. */\nfunction collectNoteTieTargets(notes: readonly Note[] | undefined, targets: Set<string>): void {\n if (!notes) return;\n for (const note of notes) {\n if (!note.ties) continue;\n for (const tie of note.ties) {\n if (tie.target) targets.add(tie.target);\n }\n }\n}\n\n/** Recursively collect all note IDs that are tie targets. */\nfunction collectTieTargets(content: readonly SequenceContent[], targets: Set<string>): void {\n for (const item of content) {\n if (item.type === \"event\") {\n collectNoteTieTargets(item.notes, targets);\n } else if (item.type === \"tuplet\") {\n collectTieTargets(item.content, targets);\n } else if (item.type === \"grace\") {\n // Grace notes may also have ties\n for (const evt of item.content) {\n collectNoteTieTargets(evt.notes, targets);\n }\n }\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sequence processing\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Allocate a MIDI channel per part, sending percussion parts to\n * `GM_DRUM_CHANNEL` and round-robining the rest across the remaining\n * 15 channels while skipping channel 9 (reserved for GM drums).\n */\nfunction allocatePartChannels(parts: readonly Score[\"parts\"][number][]): number[] {\n const MIDI_CHANNELS = 16;\n const channels: number[] = [];\n let melodicCounter = 0;\n for (const part of parts) {\n const isPerc = !!part.kit && Object.keys(part.kit).length > 0;\n if (isPerc) {\n channels.push(GM_DRUM_CHANNEL);\n } else {\n while (melodicCounter % MIDI_CHANNELS === GM_DRUM_CHANNEL) melodicCounter++;\n channels.push(melodicCounter % MIDI_CHANNELS);\n melodicCounter++;\n }\n }\n return channels;\n}\n\n/**\n * Build the `kit-component-id \u2192 MIDI note` map for a percussion part.\n * Returns undefined for non-percussion parts.\n */\nfunction buildKitMidiMap(score: Score, part: Score[\"parts\"][number]): ReadonlyMap<string, number> | undefined {\n if (!part.kit || Object.keys(part.kit).length === 0) return undefined;\n const map = new Map<string, number>();\n const sounds = score.global.sounds ?? {};\n for (const [id, comp] of Object.entries(part.kit)) {\n const sound = comp.sound ? sounds[comp.sound] : undefined;\n if (sound && typeof sound.midiNumber === \"number\") {\n map.set(id, sound.midiNumber);\n }\n }\n return map;\n}\n\n/**\n * Build the `kit-component-id \u2192 drum-kit program` override map. Only components\n * carrying a `drumKit` vendor field appear; their hits play on a dedicated drum\n * channel loaded with that GS kit program (e.g. a Tam-tam borrowed from the\n * Ethnic kit). Returns undefined when no component overrides its kit.\n */\nfunction buildKitAltProgramMap(part: Score[\"parts\"][number]): ReadonlyMap<string, number> | undefined {\n if (!part.kit) return undefined;\n let map: Map<string, number> | undefined;\n for (const [id, comp] of Object.entries(part.kit)) {\n if (typeof comp.drumKit === \"number\") {\n (map ??= new Map()).set(id, comp.drumKit);\n }\n }\n return map;\n}\n\n/**\n * Pre-scan a part to collect every note id that is the target of a tie.\n * These notes have their noteOn suppressed and instead extend the\n * preceding pending noteOff.\n */\nfunction collectPartTieTargets(part: Score[\"parts\"][number], measureOrder: readonly number[]): Set<string> {\n const tieTargets = new Set<string>();\n for (const expandedIdx of measureOrder) {\n const partMeasure = part.measures[expandedIdx];\n if (!partMeasure) continue;\n for (const seq of partMeasure.sequences) {\n collectTieTargets(seq.content, tieTargets);\n }\n }\n return tieTargets;\n}\n\n/** Timing bundle threaded into `processPart` (keeps the param count in check). */\ninterface TimelineTiming {\n readonly tempoMap: readonly TempoMapEntry[];\n readonly measureStartTimes: readonly number[];\n readonly model: TempoModel;\n readonly measureStartBeats: readonly number[];\n}\n\n/**\n * Generate every MIDI event for a single part, appending to `allEvents`.\n * Extracted from generateTimeline so neither function exceeds the\n * complexity / statement budget.\n */\nfunction processPart(\n score: Score,\n partIdx: number,\n options: TimelineOptions | undefined,\n partChannels: readonly number[],\n timing: TimelineTiming,\n measureOrder: readonly number[],\n globalMeasures: readonly GlobalMeasure[],\n holdSchedule: HoldSchedule,\n impliedDynamics: readonly ImpliedSectionDynamicAnchor[],\n allEvents: MidiEvent[],\n): void {\n const { tempoMap, measureStartTimes, model, measureStartBeats } = timing;\n const part = score.parts[partIdx]!;\n const channel = partChannels[partIdx]!;\n const gmProgram = options?.partPrograms?.[partIdx] ?? -1;\n const kitMidiMap = buildKitMidiMap(score, part);\n const kitAltProgramMap = buildKitAltProgramMap(part);\n const tieTargets = collectPartTieTargets(part, measureOrder);\n const pendingTieOffs = new Map<string, MidiEvent>();\n const legatoOutIds = collectPartLegatoOut(part);\n const pendingLegato = new Map<string, MidiEvent[]>();\n\n // Staff-local synchronous meters (`_x.viritura.staffMeters`), resolved once\n // per part against the (unexpanded) measure order so repeat/jump expansion\n // in `measureOrder` doesn't need to recompute inherited state.\n const staffMeterTable = resolvePartStaffMeterTable(part, globalMeasures);\n\n // Coupled dynamics: a single source of truth for velocity (sampled per note)\n // and CC11 (emitted up front so it spans note boundaries for held-note shaping).\n const dynamicProgram = compileDynamicProgram(\n part,\n partIdx,\n measureOrder,\n measureStartBeats,\n model,\n globalMeasures,\n gmProgram,\n impliedDynamics,\n staffMeterTable,\n );\n for (const lane of dynamicProgram.lanes.values()) {\n emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);\n }\n\n // Persistent pizz/arco + con sord. keyswitch state.\n const bowCapable = ARCO_CAPABLE_PROGRAMS.has(gmProgram);\n const muteFamily = muteFamilyForProgram(gmProgram);\n const techniqueEnabled = bowCapable || muteFamily !== null;\n let techniqueState: TechniqueState = { program: gmProgram, muted: false };\n\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const origMeasureIdx = measureOrder[expandedIdx]!;\n const partMeasure = part.measures[origMeasureIdx];\n if (!partMeasure) continue;\n\n const measureStartTime = measureStartTimes[expandedIdx]!;\n const measureStartBeat = measureStartBeats[expandedIdx]!;\n const activeTime = resolveActiveTime(globalMeasures, origMeasureIdx);\n const activeKeyFifths = resolveActiveKeyFifths(globalMeasures, origMeasureIdx);\n\n // Fermata/caesura holds for this measure shift later events within it.\n const measureHolds = holdSchedule[expandedIdx] ?? [];\n const measureSpq = model.spbAtBeat(measureStartBeat);\n // Each fermata hold is a merged span [startBeat, spanEndBeat) whose time\n // insertion lives in the tempo model at the driver-end beat (`atBeat`). A\n // carrier in the span rings through the gap automatically; the group bounds\n // let resolveFermataDuration force-extend a member that ends early.\n const fermataGroups = measureHolds\n .filter((h) => h.kind === \"fermata\")\n .map((h) => ({\n startBeat: h.startBeat ?? h.atBeat,\n spanEndBeat: h.spanEndBeat ?? h.atBeat,\n insertBeat: h.atBeat,\n }));\n\n const ctx: PartCtx = {\n tempoMap,\n model,\n measureStartBeat,\n partIndex: partIdx,\n channel,\n tieTargets,\n pendingTieOffs,\n legatoOutIds,\n pendingLegato,\n voiceKey: \"\",\n out: allEvents,\n gmProgram,\n timeSig: activeTime,\n staffMeterRatio: 1,\n keyFifths: activeKeyFifths,\n kitMidiMap,\n kitAltProgramMap,\n dynamicsEnvelope: dynamicProgram.lanes.values().next().value!.envelope,\n playbackLaneId: dynamicProgram.lanes.values().next().value!.id,\n dynamicProgram,\n sequenceStaff: 1,\n sequenceVoice: \"sequence:0\",\n measureSpq,\n fermataGroups,\n };\n\n if (techniqueEnabled) {\n techniqueState = applyMeasureTechniques(\n partMeasure.expressions,\n ctx,\n techniqueState,\n {\n bow: bowCapable,\n mute: muteFamily,\n },\n (staff) => staffMeterRatioAt(staffMeterTable, origMeasureIdx, staff),\n );\n }\n\n partMeasure.sequences.forEach((seq, seqIdx) => {\n // Per-voice identity for tie/legato pairing: the MNX voice id when present\n // (stable across measures so ties cross bar lines), else the sequence slot.\n ctx.voiceKey = seq.voice ?? `v${seqIdx}`;\n const lane = laneForSequence(dynamicProgram, partIdx, seq, seqIdx);\n ctx.sequenceStaff = seq.staff ?? 1;\n ctx.sequenceVoice = lane.voice;\n ctx.dynamicsEnvelope = lane.envelope;\n ctx.playbackLaneId = lane.id;\n const effectiveStaffMeter = staffMeterTable[origMeasureIdx]?.get(ctx.sequenceStaff);\n ctx.timeSig = effectiveStaffMeter ? effectiveStaffMeter.timeSignature : activeTime;\n ctx.staffMeterRatio = staffMeterRatioAt(staffMeterTable, origMeasureIdx, ctx.sequenceStaff);\n const firstNewEvent = allEvents.length;\n processSequence(ctx, seq, measureStartTime, measureHolds, measureSpq);\n stampPlaybackLane(allEvents, firstNewEvent, lane.id);\n stampPendingLane(ctx, lane.id);\n });\n }\n\n // Flush any remaining pending tie noteOffs\n for (const noteOff of pendingTieOffs.values()) {\n allEvents.push(noteOff);\n }\n\n // Flush deferred legato noteOffs left open at the end of the part, per voice (a\n // slur that runs to the final note with no following onset to overlap).\n flushAllLegato(pendingLegato, allEvents);\n}\n\nfunction stampPlaybackLane(events: MidiEvent[], startIndex: number, laneId: string): void {\n for (let i = startIndex; i < events.length; i++) events[i]!.playbackLaneId ??= laneId;\n}\n\nfunction stampPendingLane(ctx: PartCtx, laneId: string): void {\n for (const event of ctx.pendingTieOffs.values()) event.playbackLaneId ??= laneId;\n for (const events of ctx.pendingLegato.values()) {\n for (const event of events) event.playbackLaneId ??= laneId;\n }\n}\n\n/** Absolute time (s) of a measure-relative beat, via the continuous model. */\nfunction eventTime(ctx: PartCtx, beatOffset: number): number {\n return ctx.model.timeAtBeat(ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio);\n}\n\n/** Seconds spanned by `beats` starting at a measure-relative beat. */\nfunction eventSeconds(ctx: PartCtx, beatOffset: number, beats: number): number {\n return ctx.model.secondsForBeats(\n ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n beats * ctx.staffMeterRatio,\n );\n}\n\n/** Sample the part's coupled dynamic velocity (the noteOn level) at a measure beat. */\nfunction dynamicVelocityAt(ctx: PartCtx, measureStartTime: number, beatOffset: number): number {\n void measureStartTime;\n return noteVelocityAt(ctx.dynamicsEnvelope, eventTime(ctx, beatOffset));\n}\n\n/** Run one note event against its effective staff/voice dynamics lane and tag\n * every emitted or deferred MIDI event with that lane identity. */\nfunction processScopedNoteEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const previousEnvelope = ctx.dynamicsEnvelope;\n const previousLaneId = ctx.playbackLaneId;\n const lane = laneForScope(ctx.dynamicProgram, ctx.partIndex, event.staff ?? ctx.sequenceStaff, ctx.sequenceVoice);\n ctx.dynamicsEnvelope = lane.envelope;\n ctx.playbackLaneId = lane.id;\n const firstNewEvent = ctx.out.length;\n const beats = processNoteEvent(\n ctx,\n event,\n measureStartTime,\n beatOffset,\n dynamicVelocityAt(ctx, measureStartTime, beatOffset),\n tupletRatio,\n );\n stampPlaybackLane(ctx.out, firstNewEvent, lane.id);\n stampPendingLane(ctx, lane.id);\n ctx.dynamicsEnvelope = previousEnvelope;\n ctx.playbackLaneId = previousLaneId;\n return beats;\n}\n\n/**\n * Process a single sequence (voice), appending events to `ctx.out`.\n *\n * Fermata/caesura time insertions are baked into the continuous tempo model, so\n * every event's absolute time (`eventTime`) already reflects holds at any\n * nesting depth \u2014 no per-item offset bookkeeping is needed here.\n */\nfunction processSequence(\n ctx: PartCtx,\n seq: Sequence,\n measureStartTime: number,\n measureHolds: readonly MeasureHold[],\n spq: number,\n): void {\n void measureHolds;\n void spq;\n if (seq.fullMeasure) return;\n\n let beatCursor = 0;\n for (const item of seq.content) {\n beatCursor += processContentItem(ctx, item, measureStartTime, beatCursor, 1.0);\n }\n}\n\n/**\n * Process a single content item. Returns the number of beats consumed.\n *\n * The noteOn velocity (dynamic level) is sampled from the coupled dynamics\n * envelope at each event's onset, so it is position-aware within the measure.\n */\nfunction processContentItem(\n ctx: PartCtx,\n item: SequenceContent,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n switch (item.type) {\n case \"event\":\n return processScopedNoteEvent(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"tuplet\":\n return processTuplet(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"grace\":\n return processGrace(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"space\":\n // Space: advance time without emitting events\n // duration is [numerator, denominator] as a fraction of a whole note\n return (item.duration[0] / item.duration[1]) * 4 * tupletRatio;\n\n case \"tremolo\":\n return processTremolo(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n default:\n return 0;\n }\n}\n\n/**\n * Resolve a note's sounding duration and articulation gap scale.\n *\n * The fermata's time insertion lives in the tempo model at the group's\n * driving-note end, so a held carrier's MODEL duration already spans the gap \u2014\n * it rings through automatically (the followers shifted with it at any nesting\n * depth, so there is no \"bleed\"). We only force-extend a group member that ends\n * BEFORE the insertion beat up to the group's resume time, so staggered-end\n * fermatas still release together. A bar may have several disjoint groups; the\n * carrier is matched by onset beat. A held note isn't detached \u2192 gap scale 1.\n */\nfunction resolveFermataDuration(\n ctx: PartCtx,\n event: NoteEvent,\n naturalDurationSec: number,\n beatOffset: number,\n): { durationSec: number; durationScale: number; held: boolean } {\n if (event.fermata && event.fermata.duration !== \"none\" && ctx.fermataGroups.length > 0) {\n // Hold groups are expressed on the shared global axis. Convert this\n // staff's written onset before matching it to its ensemble group.\n const globalBeatOffset = beatOffset * ctx.staffMeterRatio;\n const group = ctx.fermataGroups.find(\n (g) => globalBeatOffset >= g.startBeat - 1e-9 && globalBeatOffset < g.spanEndBeat - 1e-9,\n );\n if (group) {\n // Resume time = model time at the (post-insertion) driver-end beat. For\n // the driver and any member ending at/after it, naturalDurationSec already\n // reaches this (the gap is inside its span); a member ending earlier is\n // extended up to it so the group releases together.\n const startTime = eventTime(ctx, beatOffset);\n const resumeTime = ctx.model.timeAtBeat(ctx.measureStartBeat + group.insertBeat);\n return { durationSec: Math.max(naturalDurationSec, resumeTime - startTime), durationScale: 1, held: true };\n }\n }\n return { durationSec: naturalDurationSec, durationScale: articulationDurationScale(event.markings), held: false };\n}\n\n/** Whether this event is interior to a slur span (legato-connected to the next note). */\nfunction isInteriorSlurNote(ctx: PartCtx, event: NoteEvent): boolean {\n return event.id !== undefined && ctx.legatoOutIds.has(event.id);\n}\n\n/**\n * Dispatch an event to a special playback mode (percussion roll, pitched\n * tremolo, or trill). Returns the consumed beats when one handled the event, or\n * `null` to fall through to the plain note path.\n *\n * Order matters: the percussion-roll branch runs before the pitched-tremolo\n * branch because the latter only subdivides `event.notes` and would silently\n * drop a kit-note roll (kit hits live in `event.kitNotes`).\n */\nfunction tryProcessSpecialEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n beats: number,\n): number | null {\n // Percussion roll: a tremolo on a kit-note sustains a single roll note for\n // the full duration instead of repeated hits (the percussion analog of the\n // 3-slash string tremolo \u2192 Tremolo Strings swap).\n if (ctx.kitMidiMap && event.markings?.tremolo && (event.kitNotes?.length ?? 0) > 0) {\n return processKitRoll(ctx, event, measureStartTime, beatOffset, baseVelocity, beats);\n }\n\n // Single-note pitched tremolo: subdivide into rapid repeated notes.\n const tremoloMarks = event.markings?.tremolo?.marks;\n if (tremoloMarks && tremoloMarks >= 1 && tremoloMarks <= 3) {\n return processSingleNoteTremolo(ctx, event, measureStartTime, beatOffset, baseVelocity, beats, tremoloMarks);\n }\n\n // Trill: alternate the principal with its diatonic upper auxiliary. Falls\n // through (returns null) if the duration is too short to trill.\n if (event.markings?.trill && tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity, beats)) {\n return beats;\n }\n\n return null;\n}\n\n/**\n * Process a NoteEvent \u2014 emit noteOn/noteOff pairs for each note.\n * Handles ties by deferring noteOff and suppressing noteOn for tied-to notes.\n * Handles single-note tremolos by subdividing into rapid repeated notes.\n */\nfunction processNoteEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n tupletRatio: number,\n): number {\n const { partIndex, channel, tieTargets, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx;\n const beats = durationBeats(event.duration) * tupletRatio;\n\n if (isRest(event)) {\n // A slur never bridges a rest: release this voice's deferred legato note at\n // its natural time rather than overlapping into the silence.\n flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out);\n return beats;\n }\n\n // Resolve any deferred legato noteOffs from the previous note against this\n // note's onset (the \"next\" note from their perspective). Done up front so it\n // also covers the tremolo/trill branches below, which return early.\n resolveVoiceLegato({\n map: ctx.pendingLegato,\n voiceKey: ctx.voiceKey,\n out: ctx.out,\n model: ctx.model,\n measureStartBeat: ctx.measureStartBeat,\n partIndex: ctx.partIndex,\n measureStartTime,\n beatOffset,\n beats,\n notes: event.notes ?? [],\n });\n\n // Special playback modes (percussion roll / pitched tremolo / trill) handle\n // the event themselves and short-circuit the plain note path below.\n const specialBeats = tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, beats);\n if (specialBeats !== null) return specialBeats;\n\n const startTime = eventTime(ctx, beatOffset);\n const timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const naturalDurationSec = eventSeconds(ctx, beatOffset, beats);\n // A fermata holds the note in place (see resolveFermataDuration); otherwise\n // normal articulation scaling applies.\n const {\n durationSec,\n durationScale,\n held: fermataHeld,\n } = resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset);\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n\n // Interior slur note: defer its release so it overlaps the next note (legato).\n // A fermata-held note is exempt: it must ring through its hold rather than be\n // pinned to the next note's onset by the legato resolver (which would discard\n // the fermata extension and release the note at its natural end).\n const legatoOut = !fermataHeld && isInteriorSlurNote(ctx, event);\n\n for (const note of event.notes ?? []) {\n // Pitches in the Viritura model are stored as concert (sounding) pitch.\n // The part's transposition is rendering metadata only \u2014 no adjustment needed.\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n\n const noteId = note.id ?? \"\";\n const isTiedTo = tieTargets.has(noteId);\n const hasTieOut = note.ties?.some((t) => t.target) ?? false;\n\n if (isTiedTo) {\n extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut);\n } else {\n emitFreshNote(\n ctx,\n midiNote,\n startTime,\n timingOffset,\n durationSec,\n durationScale,\n velocity,\n hasTieOut,\n legatoOut,\n {\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n },\n );\n }\n }\n\n // Kit notes (MNX `kitNotes`): always on the GM drum channel.\n if (kitMidiMap) {\n const jitteredStart = Math.max(0, startTime + timingOffset);\n for (const kn of event.kitNotes ?? []) {\n const midiNote = kitNoteToMidi(kn, kitMidiMap);\n if (midiNote < 0 || midiNote > 127) continue;\n // A component may borrow a sound from another GS kit (e.g. Tam-tam from\n // the Ethnic kit); that routes the hit to a dedicated alt drum channel.\n const drumKitProgram = kitAltProgramMap?.get(kn.kitComponent);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n drumKitProgram,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + durationSec * durationScale,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n drumKitProgram,\n });\n }\n }\n\n return beats;\n}\n\n/**\n * Tie-target branch of processNoteEvent: this note continues from a tie, so\n * suppress its noteOn and either extend the pending noteOff (if the chain\n * keeps going) or finalize it with articulation scaling.\n */\nfunction extendOrFinalizeTie(\n ctx: PartCtx,\n midiNote: number,\n startTime: number,\n durationSec: number,\n durationScale: number,\n hasTieOut: boolean,\n): void {\n const { partIndex, channel, pendingTieOffs, out } = ctx;\n const tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`;\n const pending = pendingTieOffs.get(tieKey);\n if (pending) {\n if (hasTieOut) {\n // Chain continues: extend the pending noteOff further\n pending.time = startTime + durationSec;\n } else {\n // Chain ends: emit the noteOff now with articulation scaling\n pending.time = startTime + durationSec * durationScale;\n out.push(pending);\n pendingTieOffs.delete(tieKey);\n }\n }\n // Register outgoing tie if chain continues\n if (hasTieOut && !pendingTieOffs.has(tieKey)) {\n pendingTieOffs.set(tieKey, {\n type: \"noteOff\",\n time: startTime + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n}\n\n/**\n * Fresh-note branch of processNoteEvent: emit a noteOn with timing jitter\n * for humanization, then either defer the noteOff (if a tie chain starts\n * here) or emit it inline with articulation scaling.\n */\ninterface ScoreNoteMetadata {\n scoreBeat: number;\n scoreDurationBeats: number;\n scoreEventId?: string;\n}\n\nfunction emitFreshNote(\n ctx: PartCtx,\n midiNote: number,\n startTime: number,\n timingOffset: number,\n durationSec: number,\n durationScale: number,\n velocity: number,\n hasTieOut: boolean,\n legatoOut: boolean,\n metadata: ScoreNoteMetadata,\n): void {\n const { partIndex, channel, pendingTieOffs, out } = ctx;\n // Clamp to 0 so early notes don't get negative times.\n const jitteredStart = Math.max(0, startTime + timingOffset);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n ...metadata,\n });\n\n if (hasTieOut) {\n // Start of a tie chain \u2014 defer noteOff (no jitter on ties).\n const tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`;\n pendingTieOffs.set(tieKey, {\n type: \"noteOff\",\n time: startTime + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n } else {\n // Standalone or interior-slur note. Build the noteOff at its natural\n // (articulated) release; an interior-slur note is deferred so resolveLegato\n // can later push the release past the next onset (legato overlap).\n const off: MidiEvent = {\n type: \"noteOff\",\n time: jitteredStart + durationSec * durationScale,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n };\n if (legatoOut) pushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, off);\n else out.push(off);\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Single-note tremolo\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Beats per subdivision for each tremolo slash count. */\nconst TREMOLO_SUBDIV_BEATS: Record<number, number> = {\n 1: 0.5, // 8th notes\n 2: 0.25, // 16th notes\n 3: 0.125, // 32nd notes\n};\n\n/** GM solo string programs (40\u201343). */\nconst STRING_SOLO_RANGE_LO = 40;\nconst STRING_SOLO_RANGE_HI = 43;\n/** GM program 44 \u2014 Tremolo Strings. */\nconst GM_TREMOLO_STRINGS = 44;\n\n/**\n * Sustained \"roll\" sample for a base drum MIDI note. A tremolo (roll) on a\n * kit-note plays this single sustained note instead of repeated hits \u2014 the\n * percussion analog of the 3-slash string tremolo \u2192 Tremolo Strings swap.\n * Keyed by the kit-component's resolved GM percussion note. Drums without an\n * entry simply sustain their own sound (e.g. a suspended-cymbal roll).\n *\n * 38 (Acoustic Snare) \u2192 25 (Snare Roll): present in the GS Standard and\n * Orchestra kits this app loads from Shan-SGM-Pro-15.sf2.\n */\nconst KIT_ROLL_MIDI: Record<number, number> = {\n 38: 25, // Acoustic Snare \u2192 Snare Roll\n};\n\n/**\n * Process a percussion roll \u2014 a tremolo marking on a kit-note event. Emits one\n * sustained note per kit-note spanning the full written duration (no\n * subdivision), swapping to a dedicated roll sample when one exists\n * ({@link KIT_ROLL_MIDI}). This keeps a notated roll as a single long note that\n * triggers the roll/sustain sample rather than a machine-gun of repeated hits.\n */\nfunction processKitRoll(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n beats: number,\n): number {\n const { partIndex, channel, out, kitMidiMap, kitAltProgramMap, timeSig } = ctx;\n if (!kitMidiMap) return beats;\n\n const startTime = eventTime(ctx, beatOffset);\n const timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const jitteredStart = Math.max(0, startTime + timingOffset);\n const durationSec = eventSeconds(ctx, beatOffset, beats);\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n\n for (const kn of event.kitNotes ?? []) {\n const baseMidi = kitNoteToMidi(kn, kitMidiMap);\n if (baseMidi < 0 || baseMidi > 127) continue;\n const midiNote = KIT_ROLL_MIDI[baseMidi] ?? baseMidi;\n const drumKitProgram = kitAltProgramMap?.get(kn.kitComponent);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n drumKitProgram,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n drumKitProgram,\n });\n }\n\n return beats;\n}\n\n/**\n * Process a single-note tremolo \u2014 subdivide one note into rapid repeated notes.\n *\n * - 1 slash: subdivide into 8th notes\n * - 2 slashes: subdivide into 16th notes\n * - 3 slashes on string instruments (GM 40\u201343): use GM 45 Tremolo Strings sound\n * - 3 slashes on other instruments: subdivide into 32nd notes\n */\nfunction processSingleNoteTremolo(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n totalBeats: number,\n marks: number,\n): number {\n const { partIndex, channel, out, gmProgram, timeSig } = ctx;\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n const notes = event.notes ?? [];\n\n // 3-slash tremolo on solo strings: use Tremolo Strings GM patch\n if (marks === 3 && gmProgram >= STRING_SOLO_RANGE_LO && gmProgram <= STRING_SOLO_RANGE_HI) {\n const startTime = eventTime(ctx, beatOffset);\n const tremTimingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const jitteredTremStart = Math.max(0, startTime + tremTimingOffset);\n const durationSec = eventSeconds(ctx, beatOffset, totalBeats);\n\n // Program change \u2192 Tremolo Strings\n out.push({\n type: \"programChange\",\n time: jitteredTremStart - 0.001, // slightly before the note\n midiNote: 0,\n velocity: 0,\n partIndex,\n channel,\n program: GM_TREMOLO_STRINGS,\n });\n\n for (const note of notes) {\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n out.push({\n type: \"noteOn\",\n time: jitteredTremStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: totalBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({ type: \"noteOff\", time: jitteredTremStart + durationSec, midiNote, velocity: 0, partIndex, channel });\n }\n\n // Program change \u2192 restore original sound\n out.push({\n type: \"programChange\",\n time: startTime + durationSec + 0.001,\n midiNote: 0,\n velocity: 0,\n partIndex,\n channel,\n program: gmProgram,\n });\n\n return totalBeats;\n }\n\n // Measured tremolo: subdivide into repeated notes\n const subdivBeats = TREMOLO_SUBDIV_BEATS[marks] ?? 0.125;\n const numSubdivisions = Math.max(1, Math.round(totalBeats / subdivBeats));\n const actualSubdivBeats = totalBeats / numSubdivisions;\n\n for (let s = 0; s < numSubdivisions; s++) {\n const subBeatOffset = beatOffset + s * actualSubdivBeats;\n const subStartTime = eventTime(ctx, subBeatOffset);\n const subTimingOffset = timingHumanize(measureStartTime, subBeatOffset, partIndex);\n const jitteredSubStart = Math.max(0, subStartTime + subTimingOffset);\n const subDurationSec = eventSeconds(ctx, subBeatOffset, actualSubdivBeats);\n\n for (const note of notes) {\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n out.push({\n type: \"noteOn\",\n time: jitteredSubStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: actualSubdivBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredSubStart + subDurationSec * 0.9,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n }\n\n return totalBeats;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Trill\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Ideal wall-clock period per trill note (~14 attacks/sec). */\nconst TRILL_TARGET_PERIOD_SEC = 0.07;\n/** A trill note shorter than this reads as a click, not a pitch \u2014 don't emit it. */\nconst TRILL_MIN_NOTE_SEC = 0.05;\n\n/** Diatonic step cycle (C..B) for resolving the upper auxiliary. */\nconst STEP_CYCLE: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"];\n\n/** Chromatic alteration a key signature imposes on a given step (+1 sharp, -1 flat, 0 natural). */\nfunction keyAlterForStep(step: Step, fifths: number): number {\n if (fifths > 0) return SHARP_ORDER.slice(0, fifths).includes(step) ? 1 : 0;\n if (fifths < 0) return FLAT_ORDER.slice(0, -fifths).includes(step) ? -1 : 0;\n return 0;\n}\n\n/**\n * MIDI number of the trill's upper auxiliary for a principal pitch.\n *\n * The auxiliary is the diatonic step above, altered by the key signature so\n * the trill stays in-scale \u2014 unless the trill carries an explicit accidental,\n * which signals a non-diatonic (chromatic) auxiliary.\n */\nfunction trillUpperMidi(pitch: Pitch, keyFifths: number, trillAccidental: number | undefined): number {\n const idx = STEP_CYCLE.indexOf(pitch.step);\n const upStep = STEP_CYCLE[(idx + 1) % 7]!;\n const upOctave = (idx === 6 ? pitch.octave + 1 : pitch.octave) as Octave;\n const alter = trillAccidental ?? keyAlterForStep(upStep, keyFifths);\n return pitchToMidi({ step: upStep, octave: upOctave, alter });\n}\n\n/**\n * Expand a trilled note into rapid alternation between the principal note and\n * its diatonic upper auxiliary.\n *\n * Rate is computed in wall-clock time (\u224870 ms per note) rather than a fixed\n * note value, so the trill speed stays musical across tempos. The alternation\n * count is then snapped so every note divides the written duration evenly \u2014\n * keeping all notes (including the last) the same length and ending the trill\n * cleanly on the note boundary.\n *\n * Starts on the principal (lower) note. If the note is too short to fit even a\n * single perceptible alternation, returns `false` so the caller plays it plain\n * (i.e. the trill is omitted rather than emitting an imperceptible blur).\n */\nfunction tryProcessTrill(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n totalBeats: number,\n): boolean {\n const { partIndex, channel, out, timeSig, keyFifths } = ctx;\n const notes = event.notes ?? [];\n if (notes.length === 0) return false;\n\n const durationSec = eventSeconds(ctx, beatOffset, totalBeats);\n\n // Even division by the wall-clock target, then back off until each note\n // clears the perceptibility floor. Fewer than 2 notes can't read as a trill.\n let n = Math.round(durationSec / TRILL_TARGET_PERIOD_SEC);\n while (n >= 2 && durationSec / n < TRILL_MIN_NOTE_SEC) n--;\n if (n < 2) return false;\n\n const subBeats = totalBeats / n;\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n const trillAccidental = event.markings?.trill?.accidental;\n\n // Principal + upper auxiliary MIDI for each note in the (possibly chord) event.\n const pairs = notes\n .map((note) => ({\n lower: pitchToMidi(note.pitch),\n upper: trillUpperMidi(note.pitch, keyFifths, trillAccidental),\n }))\n .filter((p) => p.lower >= 0 && p.lower <= 127 && p.upper >= 0 && p.upper <= 127);\n if (pairs.length === 0) return false;\n\n for (let s = 0; s < n; s++) {\n // Start on the principal (lower) note: even steps lower, odd steps upper.\n const useUpper = s % 2 === 1;\n const subBeatOffset = beatOffset + s * subBeats;\n const subStartTime = eventTime(ctx, subBeatOffset);\n const subTimingOffset = timingHumanize(measureStartTime, subBeatOffset, partIndex);\n const jitteredStart = Math.max(0, subStartTime + subTimingOffset);\n const subDurationSec = eventSeconds(ctx, subBeatOffset, subBeats);\n\n for (const pair of pairs) {\n const midiNote = useUpper ? pair.upper : pair.lower;\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: subBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + subDurationSec * 0.9,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n }\n\n return true;\n}\n\n/**\n * Process a grace note group.\n *\n * Grace note timing follows industry-standard engravers conventions:\n * - makeTime (default) / stealPrevious: grace notes are played just before\n * the beat, stealing time from the preceding duration. The main timeline\n * cursor is NOT advanced.\n * - stealFollowing: grace notes are played at the current beat, pushing the\n * following note later. The cursor IS advanced by the total grace duration.\n *\n * Each grace note receives a fixed short duration (GRACE_NOTE_BEATS \u2248 32nd\n * note) regardless of its written value.\n */\nfunction processGrace(\n ctx: PartCtx,\n grace: Grace,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const events = grace.content;\n if (events.length === 0) return 0;\n\n const graceBeats = GRACE_NOTE_BEATS * tupletRatio;\n const totalGraceBeats = events.length * graceBeats;\n\n if (grace.graceType === \"stealFollowing\") {\n // Play at current position \u2014 main note is pushed later\n let cursor = beatOffset;\n for (const evt of events) {\n const ratio = graceBeats / durationBeats(evt.duration);\n processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio);\n cursor += graceBeats;\n }\n return totalGraceBeats;\n }\n\n // makeTime (default) / stealPrevious: play before the beat\n let cursor = beatOffset - totalGraceBeats;\n for (const evt of events) {\n const ratio = graceBeats / durationBeats(evt.duration);\n processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio);\n cursor += graceBeats;\n }\n return 0;\n}\n\n/**\n * Process a tuplet \u2014 apply the tuplet ratio and recurse into content.\n */\nfunction processTuplet(\n ctx: PartCtx,\n tuplet: {\n inner: { duration: Duration; multiple: number };\n outer: { duration: Duration; multiple: number };\n content: SequenceContent[];\n },\n measureStartTime: number,\n beatOffset: number,\n parentTupletRatio: number,\n): number {\n // Tuplet ratio: outer beats / inner beats\n // e.g., triplet: 3 eighths in the space of 2 \u2192 ratio = 2/3\n const outerBeats = durationBeats(tuplet.outer.duration) * tuplet.outer.multiple;\n const innerBeats = durationBeats(tuplet.inner.duration) * tuplet.inner.multiple;\n const ratio = (outerBeats / innerBeats) * parentTupletRatio;\n\n let cursor = beatOffset;\n for (const item of tuplet.content) {\n cursor += processContentItem(ctx, item, measureStartTime, cursor, ratio);\n }\n\n return outerBeats * parentTupletRatio;\n}\n\n/**\n * Process a multi-note tremolo \u2014 alternate between the two notes.\n */\nfunction processTremolo(\n ctx: PartCtx,\n tremolo: {\n content: NoteEvent[];\n marks: number;\n outer: { duration: Duration; multiple: number };\n },\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const outerBeats = durationBeats(tremolo.outer.duration) * tremolo.outer.multiple * tupletRatio;\n\n if (tremolo.content.length < 2) return outerBeats;\n\n const subdivisions = Math.pow(2, tremolo.marks);\n const subdivBeats = outerBeats / subdivisions;\n\n for (let s = 0; s < subdivisions; s++) {\n const noteEvent = tremolo.content[s % 2]!;\n const subBeatOffset = beatOffset + s * subdivBeats;\n const subRatio = subdivBeats / durationBeats(noteEvent.duration);\n\n processScopedNoteEvent(ctx, noteEvent, measureStartTime, subBeatOffset, subRatio);\n }\n\n return outerBeats;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Active time signature resolution\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Resolve the active key signature (circle-of-fifths) at a given measure index. */\nfunction resolveActiveKeyFifths(globalMeasures: readonly GlobalMeasure[], measureIdx: number): number {\n for (let m = measureIdx; m >= 0; m--) {\n const key = globalMeasures[m]!.key;\n if (key) return key.fifths;\n }\n return 0;\n}\n", "/**\n * Repeat & jump expansion \u2014 converts a `GlobalMeasure[]` into a linear\n * sequence of measure indices that the MIDI generator can walk straight\n * through.\n *\n * Extracted from `timeline.ts` to keep that file under the max-lines budget.\n */\n\nimport type { GlobalMeasure, Score } from \"@viritura/core\";\n\ninterface MeasureOrderHints {\n toCodaMeasureIndex?: number;\n}\n\n/**\n * Expand repeats and jumps into a linear sequence of measure indices.\n *\n * Handles:\n * - Simple repeats (repeatStart / repeatEnd)\n * - Volta brackets (endings with numbers)\n * - Jump markers (D.S. al Fine, D.S. al Coda, D.C. al Coda)\n */\nexport function expandMeasureOrder(globalMeasures: readonly GlobalMeasure[], hints?: MeasureOrderHints): number[] {\n if (globalMeasures.length === 0) return [];\n\n // Phase 1: expand repeats with volta endings\n const order = expandRepeats(globalMeasures);\n\n // Phase 2: handle jumps (D.S./D.C.)\n return expandJumps(globalMeasures, order, hints?.toCodaMeasureIndex);\n}\n\nexport function detectToCodaMeasureIndex(score: Pick<Score, \"parts\">): number {\n for (const part of score.parts) {\n for (let i = 0; i < part.measures.length; i++) {\n const expressions = part.measures[i]?.expressions;\n if (!expressions || expressions.length === 0) continue;\n if (expressions.some((expr) => /\\bto\\s*coda\\b/i.test(expr.text))) {\n return i;\n }\n }\n }\n return -1;\n}\n\n/** A volta ending spanning measure indices [startIdx, endIdx], taken on the listed pass numbers. */\ninterface EndingInfo {\n startIdx: number;\n endIdx: number;\n numbers: number[];\n}\n\n/**\n * Append one repeat-pass to `order`, skipping measures that belong to a\n * volta ending whose `numbers` list doesn't include the current pass.\n * Extracted so the volta loop in `expandRepeats` doesn't exceed max-depth.\n */\nfunction appendVoltaPass(\n order: number[],\n endings: readonly EndingInfo[],\n pass: number,\n startIdx: number,\n endIdx: number,\n): void {\n for (let j = startIdx; j <= endIdx; j++) {\n const inEnding = endings.find((e) => j >= e.startIdx && j <= e.endIdx);\n if (inEnding && !inEnding.numbers.includes(pass)) continue;\n order.push(j);\n }\n}\n\n/**\n * Expand repeat markers and volta endings into a linear measure order.\n */\nfunction expandRepeats(globalMeasures: readonly GlobalMeasure[]): number[] {\n const order: number[] = [];\n const len = globalMeasures.length;\n let i = 0;\n\n while (i < len) {\n const gm = globalMeasures[i]!;\n\n if (gm.repeatEnd) {\n const repeatTimes = gm.repeatEnd.times ?? 2;\n\n // Find the matching repeatStart (scan backwards from current)\n let repeatStartIdx = 0;\n for (let j = i; j >= 0; j--) {\n if (globalMeasures[j]!.repeatStart) {\n repeatStartIdx = j;\n break;\n }\n }\n\n // Remove measures already added from the repeat range\n // (they were added as regular measures before we knew about the repeat)\n while (order.length > 0 && order[order.length - 1]! >= repeatStartIdx && order[order.length - 1]! <= i) {\n order.pop();\n }\n\n // Collect volta endings in this repeat section\n const endings: EndingInfo[] = [];\n for (let j = repeatStartIdx; j <= i; j++) {\n const ending = globalMeasures[j]!.ending;\n if (ending) {\n endings.push({\n startIdx: j,\n endIdx: Math.min(j + ending.duration - 1, i),\n numbers: ending.numbers,\n });\n }\n }\n\n if (endings.length > 0) {\n for (let pass = 1; pass <= repeatTimes; pass++) {\n appendVoltaPass(order, endings, pass, repeatStartIdx, i);\n }\n } else {\n for (let pass = 0; pass < repeatTimes; pass++) {\n for (let j = repeatStartIdx; j <= i; j++) {\n order.push(j);\n }\n }\n }\n\n i++;\n } else {\n order.push(i);\n i++;\n }\n }\n\n return order;\n}\n\nfunction appendRange(order: number[], start: number, endInclusive: number): void {\n for (let measure = start; measure <= endInclusive; measure++) order.push(measure);\n}\n\nfunction appendAlCodaReplay(\n result: number[],\n replayStart: number,\n codaIdx: number,\n measureCount: number,\n toCodaMeasureIdx: number | undefined,\n): void {\n const replayEnd =\n toCodaMeasureIdx !== undefined && toCodaMeasureIdx >= replayStart && toCodaMeasureIdx < codaIdx\n ? toCodaMeasureIdx\n : codaIdx - 1;\n appendRange(result, replayStart, replayEnd);\n appendRange(result, codaIdx, measureCount - 1);\n}\n\n/**\n * Process jump markers in the measure order.\n */\nfunction expandJumps(\n globalMeasures: readonly GlobalMeasure[],\n initialOrder: readonly number[],\n toCodaMeasureIdx: number | undefined,\n): number[] {\n let segnoIdx = -1;\n let fineIdx = -1;\n let codaIdx = -1;\n\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n if (gm.segno) segnoIdx = m;\n if (gm.fine) fineIdx = m;\n if (gm.coda) codaIdx = m;\n }\n\n // Find the first jump in the expanded order\n let jumpOrderPos = -1;\n let jumpMeasureIdx = -1;\n for (let p = 0; p < initialOrder.length; p++) {\n const mi = initialOrder[p]!;\n if (globalMeasures[mi]!.jump) {\n jumpOrderPos = p;\n jumpMeasureIdx = mi;\n break;\n }\n }\n\n if (jumpOrderPos === -1) return [...initialOrder];\n\n const jump = globalMeasures[jumpMeasureIdx]!.jump!;\n const result = initialOrder.slice(0, jumpOrderPos + 1);\n\n switch (jump.type) {\n case \"dsalfine\":\n if (segnoIdx >= 0 && fineIdx >= 0) appendRange(result, segnoIdx, fineIdx);\n break;\n case \"dsalcoda\":\n if (segnoIdx >= 0 && codaIdx >= 0)\n appendAlCodaReplay(result, segnoIdx, codaIdx, globalMeasures.length, toCodaMeasureIdx);\n break;\n case \"dcalcoda\":\n if (codaIdx >= 0) appendAlCodaReplay(result, 0, codaIdx, globalMeasures.length, toCodaMeasureIdx);\n break;\n case \"segno\":\n if (segnoIdx >= 0) appendRange(result, segnoIdx, globalMeasures.length - 1);\n break;\n }\n\n return result;\n}\n", "/**\n * Measure repeats (simile marks) for playback.\n *\n * MNX puts a `measureRepeat` on the FIRST bar it covers and says \"repeat all\n * music in the previous N measures\". The covered bars carry no encoding of\n * their own and normally hold empty sequences, so playback has to substitute\n * the source material back in before anything downstream (notes, ties,\n * dynamics, holds) can see it.\n *\n * Substitution is deep-cloned with rewritten ids so a repeated bar does not\n * collide with its source in id-keyed lookups such as tie resolution.\n */\n\nimport type { Part, PartMeasure, Score } from \"@viritura/core\";\n\n/**\n * Map each part-measure index to the index whose music actually sounds there.\n *\n * A repeat at index `i` spanning `n` bars sources indices `i - n \u2026 i - 1`.\n * Chains resolve transitively, so a simile pointing at a bar that is itself a\n * simile reaches through to real music.\n */\nfunction buildSourceIndices(measures: readonly PartMeasure[]): number[] {\n const source = measures.map((_, index) => index);\n for (let index = 0; index < measures.length; index++) {\n const repeat = measures[index]?.measureRepeat;\n if (!repeat) continue;\n const span = Math.max(1, Math.trunc(repeat.number));\n const firstSource = index - span;\n if (firstSource < 0) continue;\n for (let offset = 0; offset < span && index + offset < measures.length; offset++) {\n // Resolve through the source's own mapping so chained similes collapse\n // to the nearest bar holding real music.\n source[index + offset] = source[firstSource + offset] ?? firstSource + offset;\n }\n }\n return source;\n}\n\n/** Rewrite every defined id in one clone and add it to the span-wide map. */\nfunction rewriteIds(node: unknown, suffix: string, remap: Map<string, string>): void {\n if (Array.isArray(node)) {\n for (const item of node) rewriteIds(item, suffix, remap);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const record = node as Record<string, unknown>;\n if (typeof record[\"id\"] === \"string\") {\n const original = record[\"id\"];\n const next = `${original}${suffix}`;\n remap.set(original, next);\n record[\"id\"] = next;\n }\n for (const value of Object.values(record)) rewriteIds(value, suffix, remap);\n}\n\n/** Relink every string reference that names an id cloned in this repeat span. */\nfunction relinkIds(node: unknown, remap: ReadonlyMap<string, string>): void {\n if (Array.isArray(node)) {\n for (const item of node) relinkIds(item, remap);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const record = node as Record<string, unknown>;\n for (const [key, value] of Object.entries(record)) {\n if (key === \"id\") continue;\n if (typeof value === \"string\") {\n const mapped = remap.get(value);\n if (mapped !== undefined) record[key] = mapped;\n } else {\n relinkIds(value, remap);\n }\n }\n}\n\n/**\n * Expand a part's simile marks into playable measures.\n *\n * Returns the part unchanged when it has no measure repeats, so the common\n * case costs one scan and no allocation.\n */\nexport function expandMeasureRepeats(part: Part): Part {\n if (!part.measures.some((measure) => measure.measureRepeat)) return part;\n\n const source = buildSourceIndices(part.measures);\n const measures = [...part.measures];\n for (let start = 0; start < part.measures.length; start++) {\n const repeat = part.measures[start]?.measureRepeat;\n if (!repeat) continue;\n const span = Math.max(1, Math.trunc(repeat.number));\n if (start < span) continue;\n\n const clones: Array<{ destination: number; measure: PartMeasure }> = [];\n for (let offset = 0; offset < span && start + offset < part.measures.length; offset++) {\n const destination = start + offset;\n const originIndex = source[destination];\n const origin = originIndex === undefined ? undefined : part.measures[originIndex];\n if (origin) clones.push({ destination, measure: structuredClone(origin) as PartMeasure });\n }\n\n // Build one remap for the complete repeated span before relinking. This is\n // what keeps cross-barline ties/slurs/glissandos inside the new iteration.\n const remap = new Map<string, string>();\n for (const clone of clones) rewriteIds(clone.measure, `~r${clone.destination}`, remap);\n for (const clone of clones) {\n relinkIds(clone.measure, remap);\n delete clone.measure.measureRepeat;\n measures[clone.destination] = clone.measure;\n }\n start += span - 1;\n }\n\n return { ...part, measures };\n}\n\n/**\n * Expand every part's simile marks so downstream passes see real music where a\n * measure repeat stands. Returns the score unchanged when no part has one.\n */\nexport function expandScoreMeasureRepeats(score: Score): Score {\n const parts = score.parts.map(expandMeasureRepeats);\n if (parts.every((part, index) => part === score.parts[index])) return score;\n return { ...score, parts };\n}\n", "import { CHORDS_PART_ID, voiceChordSymbol, type Score } from \"@viritura/core\";\nimport type { TempoBuild } from \"../tempoMap\";\nimport type { MidiEvent } from \"../types\";\n\n/** Runtime routing metadata only; never insert this lane into score.parts. */\nexport interface ChordPlaybackPart {\n id: typeof CHORDS_PART_ID;\n partIndex: number;\n name: \"Chords\";\n}\n\n/** Unsupported symbols and NC still require a visible, independently routed lane. */\nexport function getChordPlaybackPart(score: Score): ChordPlaybackPart | undefined {\n return score.global.measures.some((measure) => (measure.chordSymbols?.length ?? 0) > 0)\n ? { id: CHORDS_PART_ID, partIndex: score.parts.length, name: \"Chords\" }\n : undefined;\n}\n\ninterface ChordChange {\n beat: number;\n pitches: readonly number[];\n}\n\ninterface ChordMeasure {\n carry: readonly number[];\n changes: readonly ChordChange[];\n}\n\nexport interface ChordPlaybackProgram {\n part: ChordPlaybackPart;\n measures: readonly ChordMeasure[];\n}\n\nexport interface ChordPlaybackNote {\n id: string;\n pitch: number;\n startTime: number;\n duration: number;\n}\n\n/** Resolve harmony in written order, before repeats can change the carried state. */\nexport function compileChordPlayback(score: Score, includeGlobalChords = true): ChordPlaybackProgram | undefined {\n if (!includeGlobalChords) return undefined;\n const part = getChordPlaybackPart(score);\n if (!part) return undefined;\n let carry: readonly number[] = [];\n const measures = score.global.measures.map((measure): ChordMeasure => {\n const changes = (measure.chordSymbols ?? [])\n .map((chord): ChordChange => {\n const voicing = voiceChordSymbol(chord);\n return {\n beat: (chord.position.fraction[0] / chord.position.fraction[1]) * 4,\n pitches: [...voicing.leftHand, ...voicing.rightHand],\n };\n })\n .sort((a, b) => a.beat - b.beat);\n const result = { carry, changes };\n if (changes.length > 0) carry = changes[changes.length - 1]!.pitches;\n return result;\n });\n return { part, measures };\n}\n\n/** Continuous measures sustain; jumps release and restore the destination's written harmony. */\nexport function realizeChordPlayback(\n program: ChordPlaybackProgram,\n measureOrder: readonly number[],\n timing: TempoBuild,\n): ChordPlaybackNote[] {\n const notes: ChordPlaybackNote[] = [];\n let pitches: readonly number[] = [];\n let startBeat = 0;\n let occurrence = 0;\n const finish = (endBeat: number): void => {\n if (endBeat <= startBeat) return;\n const startTime = timing.model.timeAtBeat(startBeat);\n const duration = timing.model.timeAtBeat(endBeat) - startTime;\n for (const pitch of pitches) {\n notes.push({ id: `${CHORDS_PART_ID}:${occurrence}:${pitch}`, pitch, startTime, duration });\n }\n occurrence++;\n };\n for (let expandedIndex = 0; expandedIndex < measureOrder.length; expandedIndex++) {\n const originalIndex = measureOrder[expandedIndex]!;\n const measure = program.measures[originalIndex]!;\n const measureStart = timing.measureStartBeats[expandedIndex]!;\n if (expandedIndex === 0 || originalIndex !== measureOrder[expandedIndex - 1]! + 1) {\n finish(measureStart);\n pitches = measure.carry;\n startBeat = measureStart;\n }\n for (const change of measure.changes) {\n const beat = measureStart + change.beat;\n finish(beat);\n pitches = change.pitches;\n startBeat = beat;\n }\n }\n finish(timing.totalBeats);\n return notes;\n}\n\nexport function chordMidiEvents(\n program: ChordPlaybackProgram,\n measureOrder: readonly number[],\n timing: TempoBuild,\n): MidiEvent[] {\n return realizeChordPlayback(program, measureOrder, timing).flatMap((note): MidiEvent[] => {\n const routing = {\n partIndex: program.part.partIndex,\n playbackLaneId: program.part.id,\n channel: 0,\n midiNote: note.pitch,\n };\n return [\n { ...routing, type: \"noteOn\", time: note.startTime, velocity: 80 },\n { ...routing, type: \"noteOff\", time: note.startTime + note.duration, velocity: 0 },\n ];\n });\n}\n", "import type { DynamicGroup, DynamicValue, LayoutContent, PartMeasure, Score, SequenceContent } from \"@viritura/core\";\n\ninterface ExplicitState {\n value: DynamicValue;\n position: number;\n partIndex: number;\n measureIndex: number;\n}\n\nexport interface ImpliedSectionDynamicAnchor {\n partIndex: number;\n measureIndex: number;\n value: DynamicValue;\n sourcePartIndex: number;\n sourceMeasureIndex: number;\n position: [number, number];\n}\n\nfunction collectGroups(content: readonly LayoutContent[], score: Score, groups: number[][]): void {\n for (const node of content) {\n if (node.type === \"group\") {\n collectGroups(node.content, score, groups);\n continue;\n }\n if (node.sources.length < 2) continue;\n const indices = node.sources\n .map((source) => score.parts.findIndex((part) => part.id === source.part))\n .filter((index) => index >= 0);\n if (indices.length > 1) groups.push([...new Set(indices)]);\n }\n}\n\n/** Stable source groups are derived from every multi-source layout staff, not the active view. */\nfunction sectionGroups(score: Score): number[][] {\n const groups: number[][] = [];\n for (const layout of score.layouts ?? []) collectGroups(layout.content, score, groups);\n return [\n ...new Map(\n groups.map((group) => {\n const sorted = [...group].sort((a, b) => a - b);\n return [sorted.join(\",\"), sorted] as const;\n }),\n ).values(),\n ];\n}\n\nconst WHOLE_DURATIONS: Record<string, number> = {\n maxima: 8,\n longa: 4,\n breve: 2,\n whole: 1,\n half: 1 / 2,\n quarter: 1 / 4,\n eighth: 1 / 8,\n \"16th\": 1 / 16,\n \"32nd\": 1 / 32,\n \"64th\": 1 / 64,\n \"128th\": 1 / 128,\n \"256th\": 1 / 256,\n \"512th\": 1 / 512,\n \"1024th\": 1 / 1024,\n};\n\nfunction durationValue(duration: { base: string; dots?: number }): number {\n const base = WHOLE_DURATIONS[duration.base] ?? 0;\n let value = base;\n for (let dot = 0; dot < (duration.dots ?? 0); dot++) value += base / 2 ** (dot + 1);\n return value;\n}\n\ninterface ContentInfo {\n signature: unknown[];\n active: boolean;\n onset?: number;\n duration: number;\n}\n\nfunction contentInfo(content: readonly SequenceContent[], scale = 1): ContentInfo {\n let cursor = 0;\n let active = false;\n let onset: number | undefined;\n const signature = content.map((item) => {\n if (item.type === \"event\") {\n const sounding = !!item.notes?.length || !!item.kitNotes?.length;\n if (sounding) {\n active = true;\n onset ??= cursor;\n }\n const duration = durationValue(item.duration) * scale;\n cursor += duration;\n return {\n duration: item.duration,\n sounding,\n };\n }\n if (item.type === \"tuplet\") {\n const inner = durationValue(item.inner.duration) * item.inner.multiple;\n const outer = durationValue(item.outer.duration) * item.outer.multiple;\n const nested = contentInfo(item.content, scale * (inner > 0 ? outer / inner : 1));\n if (nested.active) {\n active = true;\n onset ??= cursor + nested.onset!;\n }\n cursor += nested.duration;\n return { type: item.type, inner: item.inner, outer: item.outer, content: nested.signature };\n }\n if (item.type === \"tremolo\") {\n const duration = durationValue(item.outer.duration) * item.outer.multiple * scale;\n if (item.content.some((event) => !!event.notes?.length || !!event.kitNotes?.length)) {\n active = true;\n onset ??= cursor;\n }\n cursor += duration;\n return { type: item.type, outer: item.outer, content: item.content.map((event) => event.duration) };\n }\n if (item.type === \"grace\") return { type: item.type, content: item.content.map((event) => event.duration) };\n if (item.type === \"space\") {\n const duration = (item.duration[1] === 0 ? 0 : item.duration[0] / item.duration[1]) * scale;\n cursor += duration;\n return { type: item.type, duration: item.duration };\n }\n return {};\n });\n return { signature, active, onset, duration: cursor };\n}\n\nfunction measureActivity(measure: PartMeasure | undefined): { active: boolean; rhythm: string; onset: number } {\n if (!measure) return { active: false, rhythm: \"\", onset: 0 };\n let active = false;\n let onset: number | undefined;\n const signature = measure.sequences.map((sequence) => {\n const content = contentInfo(sequence.content);\n if (content.active) {\n active = true;\n onset = Math.min(onset ?? Number.POSITIVE_INFINITY, content.onset ?? 0);\n }\n return { staff: sequence.staff, voice: sequence.voice, content: content.signature };\n });\n return { active, rhythm: JSON.stringify(signature), onset: onset ?? 0 };\n}\n\nfunction persistentValue(group: DynamicGroup): DynamicValue | undefined {\n if (group.type === \"immediate\") return group.value;\n if (group.type === \"accent\") return group.residualValue;\n if (group.type === \"gradual\") return group.value;\n return undefined;\n}\n\nfunction positionValue(group: DynamicGroup): number {\n const [numerator, denominator] = group.position.fraction;\n return denominator === 0 ? 0 : numerator / denominator;\n}\n\nfunction positionFraction(value: number): [number, number] {\n const denominator = 65536;\n const numerator = Math.round(value * denominator);\n const gcd = (left: number, right: number): number => {\n while (right !== 0) [left, right] = [right, left % right];\n return Math.abs(left) || 1;\n };\n const divisor = gcd(numerator, denominator);\n return [numerator / divisor, denominator / divisor];\n}\n\nfunction explicitStates(measure: PartMeasure | undefined, partIndex: number, measureIndex: number): ExplicitState[] {\n return (measure?.dynamics ?? [])\n .flatMap((group) => {\n const value = persistentValue(group);\n return value ? [{ value, position: positionValue(group), partIndex, measureIndex }] : [];\n })\n .sort((left, right) => left.position - right.position);\n}\n\nfunction agreedExplicit(states: readonly ExplicitState[]): ExplicitState | null | undefined {\n if (states.length === 0) return undefined;\n const values = new Set(states.map((state) => state.value));\n return values.size === 1 ? states[0]! : null;\n}\n\nfunction updateShared(\n states: ReadonlyMap<number, readonly ExplicitState[]>,\n activeParts: readonly number[],\n positions: readonly number[],\n ownState: Map<number, DynamicValue>,\n initial: ExplicitState | undefined,\n): ExplicitState | undefined {\n let shared = initial;\n for (const position of positions) {\n const activeAtPosition = activeParts.flatMap((partIndex) =>\n states.get(partIndex)!.filter((state) => state.position === position),\n );\n const agreement = agreedExplicit(activeAtPosition);\n if (agreement === null) shared = undefined;\n else if (agreement) shared = agreement;\n for (const partIndex of states.keys()) {\n // A dynamic written while this player rests still establishes that\n // player's own state; retain it so a later reentry never overwrites an\n // explicit instruction with a section inference.\n const own = states.get(partIndex)!.find((state) => state.position === position);\n if (own) ownState.set(partIndex, own.value);\n }\n }\n return shared;\n}\n\nfunction analyzeGroup(score: Score, group: readonly number[]): ImpliedSectionDynamicAnchor[] {\n const inferred: ImpliedSectionDynamicAnchor[] = [];\n const ownState = new Map<number, DynamicValue>();\n let shared: ExplicitState | undefined;\n\n for (let measureIndex = 0; measureIndex < score.global.measures.length; measureIndex++) {\n const activity = new Map(\n group.map((partIndex) => [partIndex, measureActivity(score.parts[partIndex]?.measures[measureIndex])]),\n );\n const activeParts = group.filter((partIndex) => activity.get(partIndex)?.active);\n const states = new Map(\n group.map((partIndex) => [\n partIndex,\n explicitStates(score.parts[partIndex]?.measures[measureIndex], partIndex, measureIndex),\n ]),\n );\n const merged =\n activeParts.length > 1 && new Set(activeParts.map((partIndex) => activity.get(partIndex)!.rhythm)).size === 1;\n const onset = merged ? Math.min(...activeParts.map((partIndex) => activity.get(partIndex)!.onset)) : 0;\n const positions = [\n ...new Set(group.flatMap((partIndex) => states.get(partIndex)!.map((state) => state.position))),\n ].sort((a, b) => a - b);\n shared = updateShared(\n states,\n activeParts,\n positions.filter((position) => !merged || position <= onset),\n ownState,\n shared,\n );\n if (merged && shared) {\n for (const partIndex of activeParts) {\n const hasExplicitAtOnset = states.get(partIndex)!.some((state) => state.position === onset);\n if (hasExplicitAtOnset || ownState.get(partIndex) === shared.value) continue;\n inferred.push({\n partIndex,\n measureIndex,\n value: shared.value,\n sourcePartIndex: shared.partIndex,\n sourceMeasureIndex: shared.measureIndex,\n position: positionFraction(onset),\n });\n ownState.set(partIndex, shared.value);\n }\n }\n\n shared = updateShared(\n states,\n activeParts,\n positions.filter((position) => merged && position > onset),\n ownState,\n shared,\n );\n }\n return inferred;\n}\n\n/** Compute playback-only implied anchors without creating notation objects. */\nexport function analyzeImpliedSectionDynamics(\n score: Score,\n): ReadonlyMap<number, readonly ImpliedSectionDynamicAnchor[]> {\n const inferred = sectionGroups(score).flatMap((group) => analyzeGroup(score, group));\n const byPart = new Map<number, ImpliedSectionDynamicAnchor[]>();\n for (const anchor of inferred) {\n const anchors = byPart.get(anchor.partIndex) ?? [];\n anchors.push(anchor);\n byPart.set(anchor.partIndex, anchors);\n }\n return byPart;\n}\n", "/**\n * Hold schedule \u2014 computes the extra time that fermatas and caesuras insert\n * into the timeline.\n *\n * A fermata holds the note it sits on (and pauses the ensemble); a caesura is\n * a grand-pause / breath that inserts silence. Both INSERT time at a beat\n * position, which shifts every later event in every part by the same amount.\n *\n * To keep the ensemble aligned, holds are computed once across ALL parts:\n * - Fermatas are grouped by OVERLAPPING time spans. Each fermata'd note spans\n * [onset, end); spans whose intervals overlap merge transitively into one\n * group (a held whole note bridges shorter fermatas under it). Non-overlapping\n * fermatas in the same bar (e.g. a hold on beat 1 and a separate hold on beat\n * 4) form SEPARATE groups \u2192 SEPARATE holds, so each ensemble pause is\n * preserved instead of being flattened to a single bar-end hold.\n * - Within each group the SHORTEST fermata'd note is the DRIVER: it defines\n * both the hold AMOUNT ((mult-1) \u00D7 its duration) AND the insertion POINT\n * (its END beat). The fermata is held IN PLACE at that note \u2014 the extra time\n * is inserted right where the short note ends, NOT dumped at the bar's end.\n * Longer fermata notes that overlap merely coexist as a hint: they sustain\n * through the inserted hold (each extends by the same `extraBeats` from its\n * own natural end) rather than each doubling their own long duration.\n * - Caesuras are max-merged per beat (a caesura at the same beat in two parts\n * is one pause).\n *\n * Holds are expressed in quarter-note BEATS (not seconds) so the tempo map can\n * convert them at the tempo in effect where they occur.\n */\n\nimport type { GlobalMeasure, Score, SequenceContent, Fermata, Duration, TimeSignature } from \"@viritura/core\";\nimport { DURATION_BEATS } from \"@viritura/core\";\nimport { resolvePartStaffMeterTable, staffMeterRatioAt } from \"./staffMeterTiming\";\n\n/** What inserted a hold \u2014 determines whether carriers sustain (fermata) or the\n * gap is pure silence (caesura). */\ntype HoldKind = \"fermata\" | \"caesura\";\n\n/** Extra time inserted at one beat position within a measure. */\nexport interface MeasureHold {\n /** Beat within the measure where the extra time is inserted. For a fermata\n * this is the DRIVING (shortest) note's END beat, so the hold lands in place\n * at that note rather than at the bar's end. For a caesura it is the\n * carrier's end. */\n atBeat: number;\n /** Extra duration to insert, in quarter-note beats. */\n extraBeats: number;\n /** Whether this hold is a fermata (held notes sustain through it) or a\n * caesura (a pure gap; carriers are not extended). */\n kind: HoldKind;\n /** Fermata only: the group's earliest fermata onset beat. A fermata'd carrier\n * whose onset falls in [startBeat, spanEndBeat) belongs to this group and\n * extends by `extraBeats`. */\n startBeat?: number;\n /** Fermata only: the group's latest fermata end beat (the span upper bound\n * for carrier matching). */\n spanEndBeat?: number;\n}\n\n/** Per expanded-measure-index list of holds, sorted by `atBeat`. */\nexport type HoldSchedule = MeasureHold[][];\n\n/** Default grand-pause / caesura length, in quarter-note beats. */\nconst CAESURA_BEATS = 1.0;\n/** A `short` caesura is a briefer breath. */\nconst CAESURA_SHORT_BEATS = 0.5;\n\n/**\n * Map a fermata's notated `duration` hint to a hold multiplier (how many times\n * the note's own length it sounds). `normal`/`auto`/unset \u2248 doubles the note.\n */\nfunction fermataMultiplier(fermata: Fermata | undefined): number {\n switch (fermata?.duration) {\n case \"none\":\n return 1.0;\n case \"veryShort\":\n return 1.25;\n case \"short\":\n return 1.5;\n case \"long\":\n return 2.5;\n case \"veryLong\":\n return 3.0;\n case \"normal\":\n case \"auto\":\n default:\n return 2.0;\n }\n}\n\n/** Caesura length in beats for a given style. */\nfunction caesuraBeats(style: string | undefined): number {\n return style === \"short\" ? CAESURA_SHORT_BEATS : CAESURA_BEATS;\n}\n\n/** Quarter-note beats for a Duration (base + dots). Local copy to avoid a\n * circular import with the timeline module. */\nfunction durationBeats(d: Duration): number {\n const base = DURATION_BEATS[d.base] ?? 1;\n if (!d.dots) return base;\n return base * (2 - Math.pow(2, -d.dots));\n}\n\n/** Resolve the active time signature at a global measure index. */\nfunction resolveTimeSig(globalMeasures: readonly GlobalMeasure[], measureIdx: number): TimeSignature {\n for (let m = measureIdx; m >= 0; m--) {\n const t = globalMeasures[m]!.time;\n if (t) return t;\n }\n return { count: 4, unit: 4 };\n}\n\n/** Quarter-note beats in a measure from its time signature. */\nfunction measureBeats(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\n/** One fermata'd note's time span within a measure, in quarter-note beats. */\ninterface FermataSpan {\n /** Onset beat (cursor position where the carrier starts). */\n onset: number;\n /** End beat (onset + real duration). */\n end: number;\n /** Real (tuplet-scaled) duration in beats. */\n duration: number;\n /** Hold multiplier from this note's fermata duration hint. */\n multiplier: number;\n}\n\n/**\n * Recursively collect fermata spans and caesuras from a content array,\n * advancing a beat cursor. `ratio` is the cumulative tuplet scaling factor\n * (1 at the top level; for a triplet of 3 eighths in the space of 2 it is 2/3),\n * so a fermata on a note INSIDE a tuplet reports its REAL sounded duration \u2014\n * e.g. a triplet eighth is ~0.33 beats, not 0.5. This is essential: the clarinet\n * solo before reh. 1 carries its fermata on a triplet eighth, and without\n * recursion the scanner only saw the half/whole-note fermatas and over-held the\n * whole bar. Returns the real beats consumed by `content`.\n */\nfunction collectFromContent(\n content: readonly SequenceContent[],\n startBeat: number,\n ratio: number,\n spans: FermataSpan[],\n caesuraByBeat: Map<number, number>,\n): number {\n let cursor = startBeat;\n for (const item of content) {\n if (item.type === \"event\") {\n const dur = durationBeats(item.duration) * ratio;\n const endBeat = cursor + dur;\n if (item.fermata && item.fermata.duration !== \"none\") {\n const mult = fermataMultiplier(item.fermata);\n if (dur > 0 && mult > 1) {\n spans.push({ onset: cursor, end: endBeat, duration: dur, multiplier: mult });\n }\n }\n if (item.markings?.caesura) {\n mergeMax(caesuraByBeat, endBeat, caesuraBeats(item.markings.caesura.style));\n }\n cursor = endBeat;\n } else if (item.type === \"tuplet\") {\n const outerBeats = durationBeats(item.outer.duration as Duration) * item.outer.multiple;\n const innerBeats = durationBeats(item.inner.duration as Duration) * item.inner.multiple;\n const childRatio = innerBeats > 0 ? (outerBeats / innerBeats) * ratio : ratio;\n collectFromContent(item.content, cursor, childRatio, spans, caesuraByBeat);\n cursor += outerBeats * ratio;\n } else if (item.type === \"tremolo\") {\n // Tremolo content carries no meaningful per-note fermata; just advance.\n cursor += durationBeats(item.outer.duration as Duration) * item.outer.multiple * ratio;\n } else if (item.type === \"space\") {\n cursor += (item.duration[0] / item.duration[1]) * 4 * ratio;\n }\n // grace: consumes no main-cursor time; never carries an ensemble fermata.\n }\n return cursor - startBeat;\n}\n\n/**\n * Scan one part's measure, collecting its fermata'd-note spans into `spans`\n * and folding its caesuras into `caesuraByBeat` (max-merged per beat). Recurses\n * into tuplets so nested fermatas report their real (scaled) duration.\n *\n * Spans are grouped by overlap LATER (across all parts) in `groupFermataSpans`,\n * so this just gathers the raw intervals \u2014 it does not unify here.\n */\nfunction collectPartMeasureHolds(\n partMeasure: Score[\"parts\"][number][\"measures\"][number] | undefined,\n originalMeasureIndex: number,\n staffMeterTable: ReturnType<typeof resolvePartStaffMeterTable>,\n spans: FermataSpan[],\n caesuraByBeat: Map<number, number>,\n): void {\n if (!partMeasure) return;\n for (const seq of partMeasure.sequences) {\n if (seq.fullMeasure) continue;\n const ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, seq.staff ?? 1);\n collectFromContent(seq.content, 0, ratio, spans, caesuraByBeat);\n }\n}\n\n/** Floating-point slop for beat comparisons. */\nconst BEAT_EPS = 1e-9;\n\n/**\n * Group fermata spans by overlapping intervals and emit one hold per group.\n *\n * Spans are sorted by onset; a span that starts before the running group's end\n * (strictly \u2014 touching spans do NOT merge, so back-to-back fermatas stay\n * separate holds) extends the group. Each group's hold amount is\n * `(mult - 1) * minDuration` where `minDuration` is the shortest span in the\n * group (ties broken toward the larger multiplier), inserted at that DRIVING\n * note's END beat so the hold lands in place at the short note. The group's\n * full [startBeat, spanEnd) is recorded so fermata carriers can be matched.\n */\nfunction groupFermataSpans(spans: FermataSpan[]): MeasureHold[] {\n if (spans.length === 0) return [];\n spans.sort((a, b) => a.onset - b.onset || a.end - b.end);\n\n const holds: MeasureHold[] = [];\n let groupStart = spans[0]!.onset;\n let groupEnd = spans[0]!.end;\n let minDur = spans[0]!.duration;\n let mult = spans[0]!.multiplier;\n let driverEnd = spans[0]!.end; // END of the shortest span = insertion point\n\n const flush = (): void => {\n const extraBeats = (mult - 1) * minDur;\n if (extraBeats > BEAT_EPS) {\n holds.push({ atBeat: driverEnd, startBeat: groupStart, spanEndBeat: groupEnd, extraBeats, kind: \"fermata\" });\n }\n };\n\n for (let i = 1; i < spans.length; i++) {\n const s = spans[i]!;\n if (s.onset < groupEnd - BEAT_EPS) {\n // Overlaps the current group \u2192 extend it.\n groupEnd = Math.max(groupEnd, s.end);\n if (s.duration < minDur - BEAT_EPS) {\n minDur = s.duration;\n mult = s.multiplier;\n driverEnd = s.end;\n } else if (Math.abs(s.duration - minDur) < BEAT_EPS && s.multiplier > mult) {\n mult = s.multiplier;\n }\n } else {\n // Disjoint \u2192 close the current group and start a new one.\n flush();\n groupStart = s.onset;\n groupEnd = s.end;\n minDur = s.duration;\n mult = s.multiplier;\n driverEnd = s.end;\n }\n }\n flush();\n return holds;\n}\n\n/** Insert or max-merge a value at a beat position. */\nfunction mergeMax(byBeat: Map<number, number>, atBeat: number, value: number): void {\n const prev = byBeat.get(atBeat) ?? 0;\n if (value > prev) byBeat.set(atBeat, value);\n}\n\n/**\n * Build the per-measure hold schedule for the expanded measure order.\n *\n * @param score The score (for part measures).\n * @param measureOrder Expanded measure indices (post repeat/jump expansion).\n * @param globalMeasures Original global measures (for caesura + time sig).\n * @returns One sorted `MeasureHold[]` per expanded measure index.\n */\nexport function buildHoldSchedule(\n score: Score,\n measureOrder: readonly number[],\n globalMeasures: readonly GlobalMeasure[],\n): HoldSchedule {\n const schedule: HoldSchedule = [];\n const staffMeterTables = score.parts.map((part) => resolvePartStaffMeterTable(part, globalMeasures));\n\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const origIdx = measureOrder[expandedIdx]!;\n const fermataSpans: FermataSpan[] = [];\n const caesuraByBeat = new Map<number, number>();\n\n for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {\n const part = score.parts[partIndex]!;\n collectPartMeasureHolds(\n part.measures[origIdx],\n origIdx,\n staffMeterTables[partIndex]!,\n fermataSpans,\n caesuraByBeat,\n );\n }\n\n // Global caesura on the measure \u2192 grand pause at the measure end.\n const globalCaesura = globalMeasures[origIdx]?.caesura;\n if (globalCaesura) {\n const mBeats = measureBeats(resolveTimeSig(globalMeasures, origIdx));\n mergeMax(caesuraByBeat, mBeats, caesuraBeats(globalCaesura.style));\n }\n\n // Fermatas: group by overlapping spans \u2192 one hold per group (see\n // groupFermataSpans). Non-overlapping fermatas yield distinct holds.\n const holds: MeasureHold[] = groupFermataSpans(fermataSpans);\n for (const [atBeat, extraBeats] of caesuraByBeat) {\n holds.push({ atBeat, extraBeats, kind: \"caesura\" });\n }\n holds.sort((a, b) => a.atBeat - b.atBeat);\n schedule.push(holds);\n }\n\n return schedule;\n}\n", "import {\n ratioToNumber,\n resolveStaffMeterTimeline,\n type EffectiveStaffMeter,\n type GlobalMeasure,\n type Part,\n type TimeSignature,\n} from \"@viritura/core\";\n\nexport type StaffMeterTable = readonly ReadonlyMap<number, EffectiveStaffMeter>[];\n\n/** Resolve the active global time signature at a measure index. */\nexport function resolveActiveTime(globalMeasures: readonly GlobalMeasure[], measureIndex: number): TimeSignature {\n for (let index = measureIndex; index >= 0; index--) {\n const time = globalMeasures[index]?.time;\n if (time) return time;\n }\n return { count: 4, unit: 4 };\n}\n\n/** Resolve every effective staff-local meter for one part. */\nexport function resolvePartStaffMeterTable(part: Part, globalMeasures: readonly GlobalMeasure[]): StaffMeterTable {\n const globalTimeAt = globalMeasures.map((_, index) => resolveActiveTime(globalMeasures, index));\n return resolveStaffMeterTimeline(\n part.measures.map((measure) => measure.staffMeters),\n globalTimeAt,\n ).table;\n}\n\n/** Scale written beats on a staff onto the shared global-measure axis. */\nexport function staffMeterRatioAt(table: StaffMeterTable, measureIndex: number, staff: number | undefined): number {\n if (staff === undefined) return 1;\n const effective = table[measureIndex]?.get(staff);\n return effective ? ratioToNumber(effective.ratioToGlobal) : 1;\n}\n", "import {\n DURATION_BEATS,\n type GlobalMeasure,\n type Score,\n type SequenceContent,\n type TimeSignature,\n} from \"@viritura/core\";\nimport type { HoldSchedule } from \"./holds\";\n\ntype PlaybackGlobalMeasure = GlobalMeasure & { __playbackNominalTime?: TimeSignature };\n\nfunction contentBeats(content: readonly SequenceContent[]): number {\n let beats = 0;\n for (const item of content) {\n switch (item.type) {\n case \"event\":\n beats += durationBeats(item.duration.base, item.duration.dots);\n break;\n case \"space\":\n beats += (item.duration[0] / item.duration[1]) * 4;\n break;\n case \"tuplet\":\n case \"tremolo\":\n beats += durationBeats(item.outer.duration.base, item.outer.duration.dots) * item.outer.multiple;\n break;\n case \"grace\":\n break;\n }\n }\n return beats;\n}\n\nfunction durationBeats(base: keyof typeof DURATION_BEATS, dots?: number): number {\n const beats = DURATION_BEATS[base];\n return dots ? beats * (2 - Math.pow(2, -dots)) : beats;\n}\n\nfunction writtenMeasureBeats(score: Score, measureIndex: number, minimumBeats: number): number {\n return score.parts.reduce((maxBeats, part) => {\n const measure = part.measures[measureIndex];\n if (!measure) return maxBeats;\n return Math.max(\n maxBeats,\n ...measure.sequences\n .filter((sequence) => !sequence.fullMeasure)\n .map((sequence) => contentBeats(sequence.content)),\n );\n }, minimumBeats);\n}\n\n/**\n * Playback-only global measures for pickups and open-meter cadenzas. The\n * displayed time signature remains untouched; timing spans the longest written\n * sequence so every part advances together when the shortened measure ends.\n */\nexport function playbackGlobalMeasures(score: Score, measureOrder: readonly number[]): GlobalMeasure[] {\n let activeBeats = 4;\n return measureOrder.map((measureIndex) => {\n const measure = score.global.measures[measureIndex]!;\n if (measure.time) activeBeats = (measure.time.count * 4) / measure.time.unit;\n if (measureIndex === 0 && measure.number === 0) {\n const pickupBeats = writtenMeasureBeats(score, measureIndex, 0);\n if (pickupBeats > 0) {\n return {\n ...measure,\n __playbackNominalTime: measure.time ? { ...measure.time } : undefined,\n time: {\n ...measure.time,\n count: pickupBeats,\n unit: 4,\n },\n } as PlaybackGlobalMeasure;\n }\n }\n if (measure.time?.display !== \"senzaMisura\") return measure;\n\n const cadenzaBeats = writtenMeasureBeats(score, measureIndex, activeBeats);\n return {\n ...measure,\n time: {\n ...measure.time,\n count: cadenzaBeats,\n unit: 4,\n },\n };\n });\n}\n\n/** Visual fermatas in an open-meter cadenza do not introduce a second delay. */\nexport function suppressCadenzaFermataHolds(measures: readonly GlobalMeasure[], holds: HoldSchedule): HoldSchedule {\n return holds.map((measureHolds, index) =>\n measures[index]?.time?.display === \"senzaMisura\"\n ? measureHolds.filter((hold) => hold.kind !== \"fermata\")\n : [...measureHolds],\n );\n}\n", "/**\n * Slur legato \u2014 connects notes under a slur by overlapping their releases.\n *\n * Splits into two phases:\n * 1. A pre-scan (`collectPartLegatoOut`) marks every event id that is interior\n * to a slur span (covered by a slur and followed by another note still\n * under it). The slur's final note is excluded \u2014 it takes a normal release.\n * 2. At emit time the timeline defers an interior note's `noteOff` and resolves\n * it against the next note's onset (`resolveLegato`), or flushes it at its\n * natural time when no next note follows (`flushLegatoNatural`).\n */\n\nimport type { Note, NoteEvent, Score, SequenceContent } from \"@viritura/core\";\nimport { pitchToMidi } from \"@viritura/core\";\nimport type { MidiEvent } from \"./types\";\nimport type { TempoModel } from \"./tempoModel\";\nimport { timingHumanize } from \"./dynamics\";\n\n/**\n * Legato overlap for slurred notes, in seconds. An interior slur note's release\n * is pushed just past the next note's onset so the two voices briefly overlap \u2014\n * the next attack masks the previous release, reading as a connected (legato)\n * line rather than re-articulated d\u00E9tach\u00E9 notes. Standard performance practice:\n * notes under a slur are played without separation. The overlap is capped to\n * the next note's own duration (see `resolveLegato`) so the overhang can never\n * reach the note after it.\n */\nconst LEGATO_OVERLAP_SEC = 0.02;\n\n/**\n * Flatten a voice's content into an ordered list of note events, recursing into\n * tuplets and unwrapping grace/tremolo containers. The order matches the\n * notated reading order, which is what \"interior to a slur span\" is defined\n * against.\n */\nfunction collectVoiceEvents(content: readonly SequenceContent[], out: NoteEvent[]): void {\n for (const item of content) {\n if (item.type === \"event\") {\n out.push(item);\n } else if (item.type === \"tuplet\") {\n collectVoiceEvents(item.content, out);\n } else if (item.type === \"grace\") {\n for (const evt of item.content) out.push(evt);\n } else if (item.type === \"tremolo\") {\n for (const evt of item.content) out.push(evt);\n }\n }\n}\n\n/**\n * Pre-scan a part to collect every event id that is *interior* to a slur span \u2014\n * i.e. covered by a slur and followed by another note still under that slur.\n * These events are played legato: their release overlaps the next note's onset.\n * The slur's final note is excluded (it takes a normal, articulated release).\n *\n * Events are grouped by voice (so a slur only connects notes within its own\n * voice) and scanned in score order. Repeats are not expanded: ids are unique\n * in the source, and an interior id stays interior on every repeat pass.\n */\nexport function collectPartLegatoOut(part: Score[\"parts\"][number]): Set<string> {\n const voices = new Map<string, NoteEvent[]>();\n for (const partMeasure of part.measures) {\n if (!partMeasure) continue;\n partMeasure.sequences.forEach((seq, seqIdx) => {\n const voiceKey = seq.voice ?? `#${seqIdx}`;\n let list = voices.get(voiceKey);\n if (!list) {\n list = [];\n voices.set(voiceKey, list);\n }\n collectVoiceEvents(seq.content, list);\n });\n }\n\n const legatoOut = new Set<string>();\n for (const events of voices.values()) {\n const indexById = new Map<string, number>();\n events.forEach((evt, i) => {\n if (evt.id) indexById.set(evt.id, i);\n });\n events.forEach((evt, startIdx) => {\n if (!evt.slurs) return;\n for (const slur of evt.slurs) {\n const endIdx = slur.target ? indexById.get(slur.target) : undefined;\n if (endIdx === undefined || endIdx <= startIdx) continue;\n // Mark every event from the slur start up to (not including) its end.\n for (let i = startIdx; i < endIdx; i++) {\n const id = events[i]!.id;\n if (id) legatoOut.add(id);\n }\n }\n });\n }\n return legatoOut;\n}\n\n/** Collect the in-range MIDI notes of an event into a set. */\nfunction midiNotesOf(notes: readonly Note[]): Set<number> {\n const set = new Set<number>();\n for (const note of notes) {\n const m = pitchToMidi(note.pitch);\n if (m >= 0 && m <= 127) set.add(m);\n }\n return set;\n}\n\n/**\n * Resolve all deferred legato noteOffs against the next note's onset.\n *\n * - Distinct pitch: release overlaps the next attack by `LEGATO_OVERLAP_SEC`,\n * capped to the next note's own duration so the overhang ends within that\n * note and never reaches the note after it.\n * - Repeated pitch (same MIDI note): no overlap \u2014 release exactly at the next\n * onset. Overlapping a note with itself would steal/cut the voice; a gapless\n * release-then-attack instead re-articulates the repeated note cleanly.\n */\nfunction resolveLegato(\n pendingLegato: MidiEvent[],\n out: MidiEvent[],\n nextOnset: number,\n nextDurationSec: number,\n nextNotes: readonly Note[],\n): void {\n const nextMidi = midiNotesOf(nextNotes);\n const overlap = Math.min(LEGATO_OVERLAP_SEC, Math.max(0, nextDurationSec));\n for (const off of pendingLegato) {\n off.time = nextMidi.has(off.midiNote) ? nextOnset : nextOnset + overlap;\n out.push(off);\n }\n pendingLegato.length = 0;\n}\n\n/**\n * Flush deferred legato noteOffs at their natural (un-extended) time. Used when\n * the slur runs into a rest or the end of the part \u2014 there is no next note to\n * overlap, so the note releases as written.\n */\nfunction flushLegatoNatural(pendingLegato: MidiEvent[], out: MidiEvent[]): void {\n for (const off of pendingLegato) out.push(off);\n pendingLegato.length = 0;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Per-voice deferral buckets\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// A part's deferred legato offs are bucketed BY VOICE so a slur in one voice\n// isn't resolved (or stolen) by a note in another \u2014 critical for multi-voice\n// parts like piano where two hands share pitches.\n\n/** Append a deferred legato off to its voice's bucket (created on demand). */\nexport function pushVoiceLegato(map: Map<string, MidiEvent[]>, voiceKey: string, off: MidiEvent): void {\n const bucket = map.get(voiceKey);\n if (bucket) bucket.push(off);\n else map.set(voiceKey, [off]);\n}\n\n/** Flush one voice's deferred legato offs at their natural time (rest boundary). */\nexport function flushVoiceLegato(map: Map<string, MidiEvent[]>, voiceKey: string, out: MidiEvent[]): void {\n const bucket = map.get(voiceKey);\n if (bucket) flushLegatoNatural(bucket, out);\n}\n\n/** Flush every voice's remaining deferred legato offs (end of part). */\nexport function flushAllLegato(map: Map<string, MidiEvent[]>, out: MidiEvent[]): void {\n for (const bucket of map.values()) flushLegatoNatural(bucket, out);\n}\n\n/** Args for `resolveVoiceLegato` \u2014 the current voice's deferred offs against the next onset. */\nexport interface ResolveVoiceLegatoArgs {\n map: Map<string, MidiEvent[]>;\n voiceKey: string;\n out: MidiEvent[];\n /** Continuous tempo model (timing source). */\n model: TempoModel;\n /** Global beat at the current measure's start. */\n measureStartBeat: number;\n partIndex: number;\n /** Measure-start time (seconds) \u2014 used only as the deterministic jitter seed. */\n measureStartTime: number;\n beatOffset: number;\n beats: number;\n notes: readonly Note[];\n}\n\n/**\n * Resolve the current voice's deferred legato offs against the event at\n * (`measureStartBeat + beatOffset`). No-op when that voice has nothing pending.\n * The onset uses the same jittered formula as the noteOn so the overlap is exact.\n */\nexport function resolveVoiceLegato(a: ResolveVoiceLegatoArgs): void {\n const bucket = a.map.get(a.voiceKey);\n if (!bucket || bucket.length === 0) return;\n const onset = Math.max(\n 0,\n a.model.timeAtBeat(a.measureStartBeat + a.beatOffset) +\n timingHumanize(a.measureStartTime, a.beatOffset, a.partIndex),\n );\n const nextDur = a.model.secondsForBeats(a.measureStartBeat + a.beatOffset, a.beats);\n resolveLegato(bucket, a.out, onset, nextDur, a.notes);\n}\n", "/**\n * Articulation-based velocity shaping and metric/humanize offsets.\n *\n * The dynamic level \u2192 {velocity, CC11} mapping lives in `dynamicsEnvelope.ts`\n * (the coupled-dynamics system). This module holds the per-note attack-character\n * adjustments layered on top: accents, articulation duration scaling, metric\n * accent, and deterministic velocity/timing humanization.\n */\n\nimport type { Markings, TimeSignature } from \"@viritura/core\";\n\n/**\n * Apply articulation-based velocity adjustments.\n *\n * - accent: +20 (capped at 127)\n * - strongAccent (marcato): +30 (capped at 127)\n * - stress: +15\n * - unstress: \u221215 (floored at 1)\n * - softAccent: +10\n */\nexport function applyArticulationVelocity(baseVelocity: number, markings: Markings | undefined): number {\n if (!markings) return baseVelocity;\n\n let v = baseVelocity;\n if (markings.accent) v += 20;\n if (markings.strongAccent) v += 30;\n if (markings.stress) v += 15;\n if (markings.unstress) v -= 15;\n if (markings.softAccent) v += 10;\n\n return Math.max(1, Math.min(127, v));\n}\n\n/**\n * Compute the duration scaling factor for articulation markings.\n *\n * - staccato: 50% of notated duration\n * - staccatissimo: 25% of notated duration\n * - spiccato: 30% of notated duration\n * - tenuto: 100% of notated duration\n * - default (no marking): 90% (slight separation between notes)\n *\n * standard engraving practice defaults \u2014 staccato=50%, staccatissimo=33%,\n * tenuto=100%, default=~90%.\n */\nexport function articulationDurationScale(markings: Markings | undefined): number {\n if (!markings) return 0.9;\n if (markings.staccatissimo) return 0.25;\n if (markings.spiccato) return 0.3;\n if (markings.staccato) return 0.5;\n if (markings.tenuto) return 1.0;\n return 0.9;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Metric accent \u2014 subtle velocity shaping by beat position\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Maximum velocity offset for metric accent (applied at the strongest beat).\n * Kept small to remain subtle: \u00B16 on a 0\u2013127 scale (~5%).\n */\nconst METRIC_ACCENT_RANGE = 6;\n\n/**\n * Compute a metric accent velocity offset for a given beat position.\n *\n * Uses hierarchical subdivision weighting: each time you halve the beat grid,\n * positions that fall on the finer grid get progressively weaker. The result\n * is a signed offset (positive = stronger, negative = weaker) that should be\n * added to the base velocity.\n *\n * For simple meters (2/4, 3/4, 4/4, 2/2, etc.) this produces the natural\n * strong/weak pattern:\n * 4/4: beat 1 strongest, 3 strong, 2 & 4 weak; 8ths alternate, 16ths finer.\n *\n * **Future work (compound / asymmetric meters):**\n * This function currently assumes beats are grouped in powers of 2 from the\n * measure downbeat. For compound meters like 6/8 (2 groups of 3 eighth notes)\n * or 12/8 (4 groups of 3), the strong-beat hierarchy is different \u2014 e.g. in\n * 6/8, beat 4 (the dotted-quarter group boundary) should be nearly as strong\n * as beat 1, not weak. Similarly, asymmetric meters like 5/8 (2+3 or 3+2) or\n * 7/8 (2+2+3) have user-defined beat groupings.\n *\n * Once Viritura supports user-configurable beat/beam grouping per time\n * signature (similar to Dorico's beat grouping editor for complex meters),\n * this function should accept the beat group boundaries and compute metric\n * weight relative to each group's downbeat rather than the measure downbeat.\n * The grouping data would define which beats are \"strong\" within each\n * subdivision group, enabling correct metric accenting for any meter.\n *\n * @param beatOffset - Beat position within the measure (in quarter-note beats, 0-based)\n * @param timeSignature - Active time signature\n * @returns Signed velocity offset to add to base velocity (clamped later)\n */\nexport function metricAccentOffset(beatOffset: number, timeSignature: TimeSignature): number {\n const measureBeats = (timeSignature.count / timeSignature.unit) * 4;\n if (measureBeats <= 0) return 0;\n\n // Normalize position to [0, measureBeats)\n const pos = ((beatOffset % measureBeats) + measureBeats) % measureBeats;\n\n // Compute metric weight by checking which subdivision level this beat falls on.\n // Level 0 = downbeat (pos \u2248 0), highest weight.\n // Level 1 = falls on half-measure grid but not downbeat.\n // Level 2 = falls on quarter-measure grid but not half, etc.\n // Weight decreases by 1 per level. Maximum depth = 5 (down to 64th-note grid).\n const MAX_DEPTH = 5;\n let level = MAX_DEPTH; // default: finest subdivision\n\n // Check from coarsest to finest grid\n let grid = measureBeats; // full measure\n for (let d = 0; d <= MAX_DEPTH; d++) {\n // Check if pos falls on this grid (within tolerance for floating-point)\n const remainder = pos % grid;\n if (remainder < 1e-6 || grid - remainder < 1e-6) {\n level = d;\n break;\n }\n grid /= 2;\n }\n\n // Map level to velocity offset:\n // level 0 (downbeat) \u2192 +RANGE\n // level MAX_DEPTH (finest) \u2192 -RANGE\n // Linear interpolation between them\n const normalized = level / MAX_DEPTH; // 0 = strongest, 1 = weakest\n return Math.round(METRIC_ACCENT_RANGE * (1 - 2 * normalized));\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Velocity humanization \u2014 deterministic per-note jitter\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Maximum velocity jitter for humanization.\n *\n * Kept at \u00B12 to preserve the metric accent hierarchy:\n * Metric accent offsets: +6, +4, +1, -1, -4, -6 (levels 0\u20135)\n * Minimum gap between non-adjacent levels: 5 (downbeat +6 vs quarter +1)\n * With \u00B12 jitter, downbeat worst-case (+4) > quarter best-case (+3) \u2713\n *\n * Only adjacent subdivision levels (e.g. downbeat vs half-measure) can\n * occasionally overlap, which is musically natural \u2014 beat 3 in 4/4 is\n * nearly as strong as beat 1 in real performance.\n */\nconst HUMANIZE_MAX = 2;\n\n/**\n * Compute a deterministic velocity jitter for a note at a given position.\n *\n * Uses a hash of the measure's absolute time and the beat offset to produce\n * a consistent per-note offset. The same passage always sounds the same on\n * replay, but different notes within and across measures get different jitter\n * values, eliminating the \"machine gun\" effect of identical velocities.\n *\n * @param measureStartTime - Absolute start time of the measure in seconds\n * @param beatOffset - Beat position within the measure (quarter-note beats)\n * @returns Integer velocity offset in [-HUMANIZE_MAX, +HUMANIZE_MAX]\n */\nexport function velocityHumanize(measureStartTime: number, beatOffset: number): number {\n // Deterministic pseudo-random hash: sin(x) * large_prime produces chaotic\n // fractional parts, giving a well-distributed [0,1) value per input pair.\n // This is a standard technique from GLSL shader noise functions.\n const x = measureStartTime * 12.9898 + beatOffset * 78.233;\n const hash = Math.abs(Math.sin(x) * 43758.5453) % 1.0;\n return Math.round(hash * 2 * HUMANIZE_MAX - HUMANIZE_MAX);\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Timing humanization\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Maximum timing jitter in seconds (\u00B115 ms). */\nconst TIMING_JITTER_MAX = 0.015;\n\n/**\n * Deterministic per-note onset timing jitter.\n *\n * Real players never attack at exactly the same instant. This adds a small\n * random offset (\u00B115 ms) to each note's onset time based on a hash of the\n * measure time, beat position, and part index. Using different magic\n * constants from velocityHumanize ensures timing and velocity jitter are\n * uncorrelated.\n *\n * The part index is included so that two parts playing the same rhythm get\n * different timing offsets, which is essential for realism in unisons.\n *\n * @param measureStartTime - Absolute start time of the measure in seconds\n * @param beatOffset - Beat position within the measure (quarter-note beats)\n * @param partIndex - Part index (so unison parts get different offsets)\n * @returns Timing offset in seconds in [-TIMING_JITTER_MAX, +TIMING_JITTER_MAX]\n */\nexport function timingHumanize(measureStartTime: number, beatOffset: number, partIndex: number): number {\n // Different constants from velocityHumanize to decorrelate timing and velocity.\n const x = measureStartTime * 17.1391 + beatOffset * 53.7829 + partIndex * 91.4173;\n const hash = Math.abs(Math.sin(x) * 29587.7139) % 1.0;\n return hash * 2 * TIMING_JITTER_MAX - TIMING_JITTER_MAX;\n}\n", "/**\n * Playing-technique keyswitching for the MIDI timeline \u2014 pizz/arco bowing and\n * con sord./open muting, applied per measure as persistent state.\n *\n * Split out of `timeline.ts` (it crossed the 800-line file limit). This is the\n * self-contained \"performance technique\" sub-concept: it parses technique text\n * directions and emits the corresponding `programChange` / `controlChange`\n * events into the part's output buffer. The per-part traversal context\n * (`PartCtx`) and the `fractionToBeats` helper are shared back from\n * `timeline.ts`; `beatOffsetToTime` comes from the tempo map.\n */\nimport type { TextExpression } from \"@viritura/core\";\nimport { type PartCtx, fractionToBeats } from \"./timeline\";\n\n/** GM program 45 \u2014 Pizzicato Strings. */\nconst GM_PIZZICATO_STRINGS = 45;\n\n/**\n * Baseline GM programs for which pizz/arco keyswitching is meaningful:\n * solo strings (40\u201343) and string ensembles (48\u201349). On any other baseline\n * a \"pizz.\"/\"arco\" text direction is ignored for playback.\n */\nexport const ARCO_CAPABLE_PROGRAMS = new Set([40, 41, 42, 43, 48, 49]);\n\n/** Instrument family for mute (con sord.) handling, derived from GM program. */\nexport type MuteFamily = \"strings\" | \"brass\";\n\n/**\n * Classify a GM program into a mute-capable family. Strings (40\u201351, incl.\n * solo + ensemble + synth strings) and brass (56\u201363). Winds and everything\n * else return null \u2014 a \"con sord.\" direction there is ignored for playback.\n */\nexport function muteFamilyForProgram(program: number): MuteFamily | null {\n if (program >= 40 && program <= 51) return \"strings\";\n if (program >= 56 && program <= 63) return \"brass\";\n return null;\n}\n\n// MIDI controllers used to approximate a mute via the synth's per-voice filter.\n// These are per-channel (= per-part), applied inside the shared synth, so no\n// audio-graph rework is needed. A mute is TIMBRE only here (CC 74/71); it does\n// NOT touch level. CC 7 (channel volume) belongs to the mixer, and CC 11\n// (expression) is owned exclusively by the coupled dynamics system.\n/** CC 74 \u2014 Brightness (low-pass filter cutoff; 64 = neutral, lower = darker). */\nconst CC_BRIGHTNESS = 74;\n/** CC 71 \u2014 Harmonic Intensity (filter resonance; 64 = neutral). */\nconst CC_RESONANCE = 71;\n\n/** Filter curve applied to a channel for a given mute state (timbre only). */\ninterface MuteCurve {\n brightness: number;\n resonance: number;\n}\n\n/** Per-family muted curves and the shared \"open\" (restored) curve. */\nconst MUTE_CURVES: Record<MuteFamily, MuteCurve> = {\n // con sordino: soft, veiled \u2014 roll off brilliance, flat resonance.\n strings: { brightness: 44, resonance: 70 },\n // straight/Harmon mute: pinched and nasal, NOT dark. A muted trumpet keeps\n // (even emphasizes) its upper harmonics, so the cutoff stays near neutral \u2014\n // a hard lowpass here silences high brass (the trumpet sits above the cutoff).\n // The nasal color comes mostly from resonance.\n brass: { brightness: 58, resonance: 84 },\n};\n/** Neutral curve restored on \"senza sord.\" / \"open\". */\nconst OPEN_CURVE: MuteCurve = { brightness: 64, resonance: 64 };\n\n/** A persistent technique direction parsed from an expression. */\nexport type TechniqueAction = { kind: \"bow\"; pizz: boolean } | { kind: \"mute\"; muted: boolean };\n\n/** Normalized words that select pizzicato. */\nconst PIZZ_WORDS = new Set([\"pizz\", \"pizzicato\"]);\n/** Normalized words that return to ordinary (arco) bowing. */\nconst ARCO_WORDS = new Set([\"arco\", \"ord\", \"ordinario\", \"naturale\", \"nat\"]);\n/** Normalized words that engage a mute (con sordino). */\nconst MUTE_ON_WORDS = new Set([\"consord\", \"consordino\", \"consordini\", \"muted\", \"mute\", \"sord\", \"sordino\"]);\n/** Normalized words that remove a mute (senza sordino / open). */\nconst MUTE_OFF_WORDS = new Set([\"senzasord\", \"senzasordino\", \"senzasordini\", \"open\", \"viasord\"]);\n\n/**\n * Match an expression's text against the technique vocabulary, returning the\n * persistent state change it implies (bowing or mute), or null if it isn't a\n * recognized technique. Strips punctuation/whitespace so \"pizz.\", \"(pizz.)\",\n * \"con sord.\" all match.\n */\nexport function classifyTechniqueText(text: string): TechniqueAction | null {\n const word = text.toLowerCase().replace(/[^a-z]/g, \"\");\n if (PIZZ_WORDS.has(word)) return { kind: \"bow\", pizz: true };\n if (ARCO_WORDS.has(word)) return { kind: \"bow\", pizz: false };\n if (MUTE_ON_WORDS.has(word)) return { kind: \"mute\", muted: true };\n if (MUTE_OFF_WORDS.has(word)) return { kind: \"mute\", muted: false };\n return null;\n}\n\n/** Persistent per-part technique state, threaded across measures. */\nexport interface TechniqueState {\n /** Active GM program (pizz vs the part baseline). */\n program: number;\n /** Whether the part is currently muted (con sord.). */\n muted: boolean;\n}\n\n/** Emit a controlChange event for one CC at a given time. */\nfunction emitCc(ctx: PartCtx, time: number, cc: number, value: number): void {\n ctx.out.push({\n type: \"controlChange\",\n time,\n midiNote: 0,\n velocity: 0,\n partIndex: ctx.partIndex,\n channel: ctx.channel,\n cc,\n value,\n });\n}\n\n/**\n * Keyswitch-style technique application for a single measure.\n *\n * Scans the measure's text expressions for pizz/arco and con sord./open\n * markings and emits the corresponding control events whenever a state\n * changes. The synth holds each setting until the next change, so techniques\n * persist across notes and measures \u2014 exactly like a sampler keyswitch.\n *\n * - Bowing (strings only): `pizz.` \u2192 GM Pizzicato Strings via `programChange`;\n * `arco` restores the part baseline.\n * - Mute (strings + brass): `con sord.` applies a per-family filter curve via\n * CC 74/71 (timbre only); `senza sord.`/`open` restores the neutral curve.\n *\n * Returns the updated state so the caller threads it into the next measure.\n */\nexport function applyMeasureTechniques(\n expressions: readonly TextExpression[] | undefined,\n ctx: PartCtx,\n state: TechniqueState,\n caps: { bow: boolean; mute: MuteFamily | null },\n ratioForStaff: (staff: number | undefined) => number = () => 1,\n): TechniqueState {\n if (!expressions || expressions.length === 0) return state;\n\n // Resolve technique markings and sort by beat so transitions within the\n // measure resolve in playing order.\n const marks = expressions\n .map((e) => ({\n action: classifyTechniqueText(e.text),\n beat: fractionToBeats(e.position.fraction) * ratioForStaff(e.staff),\n }))\n .filter((m): m is { action: TechniqueAction; beat: number } => m.action !== null)\n .sort((a, b) => a.beat - b.beat);\n\n let { program, muted } = state;\n for (const mark of marks) {\n const time = ctx.model.timeAtBeat(ctx.measureStartBeat + mark.beat);\n if (mark.action.kind === \"bow\" && caps.bow) {\n const target = mark.action.pizz ? GM_PIZZICATO_STRINGS : ctx.gmProgram;\n if (target === program) continue;\n program = target;\n // The global sort orders this programChange before any noteOn at the same time.\n ctx.out.push({\n type: \"programChange\",\n time,\n midiNote: 0,\n velocity: 0,\n partIndex: ctx.partIndex,\n channel: ctx.channel,\n program: target,\n });\n } else if (mark.action.kind === \"mute\" && caps.mute) {\n if (mark.action.muted === muted) continue;\n muted = mark.action.muted;\n const curve = muted ? MUTE_CURVES[caps.mute] : OPEN_CURVE;\n emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness);\n emitCc(ctx, time, CC_RESONANCE, curve.resonance);\n }\n }\n return { program, muted };\n}\n", "/**\n * Tempo map builder \u2014 walks global measures and produces a time-indexed\n * list of tempo changes for converting beat positions to absolute seconds.\n *\n * All BPM values stored in the tempo map are normalized to effective\n * quarter-note BPM. For example, \"dotted quarter = 120\" becomes\n * effective QPM = 120 \u00D7 1.5 = 180.\n */\n\nimport type { GlobalMeasure, TimeSignature } from \"@viritura/core\";\nimport { DURATION_BEATS } from \"@viritura/core\";\nimport type { TempoMapEntry } from \"./types\";\nimport type { HoldSchedule } from \"./holds\";\nimport { TempoModel, type TempoRegion, type TempoInsertion } from \"./tempoModel\";\n\ntype PlaybackGlobalMeasure = GlobalMeasure & { __playbackNominalTime?: TimeSignature };\n\nfunction playbackNominalTime(gm: GlobalMeasure): TimeSignature | undefined {\n return (gm as PlaybackGlobalMeasure).__playbackNominalTime;\n}\n\n/** Default tempo when no tempo marking is specified. */\nexport const DEFAULT_BPM = 120;\n\n/** Default time signature when none is specified. */\nconst DEFAULT_TIME: TimeSignature = { count: 4, unit: 4 };\n\n/**\n * Compute beats in a measure from its time signature.\n * Returns the number of quarter-note beats.\n */\nexport function measureBeatsFromTime(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\n/**\n * Compute the number of quarter-note beats for a tempo's note value.\n * A \"dotted quarter\" = 1.5 quarter-note beats.\n */\nexport function tempoNoteBeats(base: string, dots?: number): number {\n const baseBeats = DURATION_BEATS[base as keyof typeof DURATION_BEATS] ?? 1;\n if (!dots) return baseBeats;\n // Dot formula: total = base \u00D7 (2 \u2212 1/2^dots)\n return baseBeats * (2 - Math.pow(2, -dots));\n}\n\n/**\n * Convert a tempo marking to effective quarter-note BPM.\n * \"dotted quarter = 120\" \u2192 120 \u00D7 1.5 = 180 effective QPM.\n */\nexport function effectiveQpm(bpm: number, tempoBase: string, tempoDots?: number): number {\n return bpm * tempoNoteBeats(tempoBase, tempoDots);\n}\n\n/**\n * Build a tempo map from the global measures of a score.\n *\n * Each entry records the absolute time at which a tempo change occurs,\n * plus the measure index and beat offset. The `bpm` field stores the\n * effective quarter-note BPM (normalized from whatever note value the\n * tempo marking references).\n *\n * The first entry always starts at time 0. If no tempo is specified,\n * the default of 120 QPM is used.\n *\n * When a `holds` schedule is supplied (one entry per expanded measure), the\n * extra time that fermatas/caesuras insert is added to the running clock at\n * the measure-start tempo, so every later measure's start time is pushed back\n * uniformly \u2014 keeping all parts aligned through the hold.\n *\n * @returns tempoMap entries and cumulative measure start times (in seconds).\n */\nexport function buildTempoMap(\n globalMeasures: readonly GlobalMeasure[],\n holds?: HoldSchedule,\n): {\n tempoMap: TempoMapEntry[];\n measureStartTimes: number[];\n} {\n const tempoMap: TempoMapEntry[] = [];\n const measureStartTimes: number[] = [];\n\n let currentQpm = DEFAULT_BPM;\n let currentSpq = 60 / currentQpm;\n let currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit };\n let absoluteTime = 0;\n let hasInitialTempo = false;\n\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n const nominalTime = playbackNominalTime(gm) ?? gm.time;\n\n if (nominalTime) {\n currentTime = { count: nominalTime.count, unit: nominalTime.unit };\n }\n\n const mBeats = measureBeatsFromTime(gm.time ?? currentTime);\n measureStartTimes.push(absoluteTime);\n\n // Seconds-per-beat at the measure start \u2014 used for hold insertion so it\n // matches the timeline's within-measure event timing (which also uses the\n // measure-start tempo).\n const measureStartSpq = currentSpq;\n\n // Collect and sort tempo changes within this measure by beat position\n interface TempoChange {\n beatOffset: number;\n qpm: number;\n }\n const changes: TempoChange[] = [];\n\n if (gm.tempos && gm.tempos.length > 0) {\n for (const tempo of gm.tempos) {\n const beatOffset = tempo.location ? (tempo.location.fraction[0] / tempo.location.fraction[1]) * mBeats : 0;\n const qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);\n changes.push({ beatOffset, qpm });\n }\n changes.sort((a, b) => a.beatOffset - b.beatOffset);\n }\n\n // Walk through the measure, advancing time segment-by-segment\n let beatCursor = 0;\n for (const change of changes) {\n // Advance time from beatCursor to the change's beat offset at the current tempo\n if (change.beatOffset > beatCursor) {\n absoluteTime += (change.beatOffset - beatCursor) * currentSpq;\n beatCursor = change.beatOffset;\n }\n\n // Record the tempo change\n tempoMap.push({\n measureIndex: m,\n beatInMeasure: change.beatOffset,\n timeSeconds: absoluteTime,\n bpm: change.qpm,\n });\n\n if (m === 0 && change.beatOffset === 0) {\n hasInitialTempo = true;\n }\n\n currentQpm = change.qpm;\n currentSpq = 60 / currentQpm;\n }\n\n // Advance time for the remaining beats in the measure\n if (mBeats > beatCursor) {\n absoluteTime += (mBeats - beatCursor) * currentSpq;\n }\n\n // Insert fermata/caesura hold time for this measure (at the measure-start\n // tempo, matching the timeline's within-measure event shifting).\n const measureHolds = holds?.[m];\n if (measureHolds) {\n for (const hold of measureHolds) {\n absoluteTime += hold.extraBeats * measureStartSpq;\n }\n }\n }\n\n // Ensure there's always an initial tempo entry at time 0\n if (!hasInitialTempo) {\n tempoMap.unshift({\n measureIndex: 0,\n beatInMeasure: 0,\n timeSeconds: 0,\n bpm: DEFAULT_BPM,\n });\n }\n\n return { tempoMap, measureStartTimes };\n}\n\n/**\n * Result of {@link buildTempoModel}: a continuous {@link TempoModel} plus the\n * per-measure coordinate arrays the timeline needs to convert measure-relative\n * beats to the model's GLOBAL beat axis.\n */\nexport interface TempoBuild {\n /** Continuous, sub-bar-resolved beat\u2194time map (the single source of timing). */\n model: TempoModel;\n /** Absolute start time (seconds) of each expanded measure. */\n measureStartTimes: number[];\n /** Global quarter-note beat at each expanded measure's start (hold-free axis;\n * holds are time insertions, not beats). Add a measure-relative beat offset\n * to get the global beat for `model.timeAtBeat`. */\n measureStartBeats: number[];\n /** End of the final measure on the same beat axis, including inherited meters. */\n totalBeats: number;\n}\n\n/**\n * Build the continuous {@link TempoModel} for an expanded measure list.\n *\n * Tempo regions come from each measure's `tempos[]`, placed at their real\n * sub-bar beat (so a mid-bar tempo change takes effect mid-bar \u2014 the per-bar\n * model could not express this). A tempo with no later change holds constant\n * to the next region. Fermata/caesura holds become point time-insertions at\n * their (global) beat, so every later event \u2014 at any nesting depth \u2014 shifts\n * uniformly through the gap via `model.timeAtBeat`.\n *\n * `measureStartBeats[i]` is the cumulative global beat at measure `i`'s start;\n * `measureStartTimes[i] = model.timeAtBeat(measureStartBeats[i])`.\n */\nexport function buildTempoModel(globalMeasures: readonly GlobalMeasure[], holds?: HoldSchedule): TempoBuild {\n const regions: TempoRegion[] = [];\n const measureStartBeats: number[] = [];\n\n let cumBeats = 0;\n let currentTime: TimeSignature = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit };\n\n // Collected gradual-tempo (rit./accel.) ramps, resolved to global beats after\n // the full measureStartBeats axis is known (the ramp end may cross bar lines).\n const gradualDecls: { declIdx: number; gt: NonNullable<GlobalMeasure[\"gradualTempo\"]>; startBeat: number }[] = [];\n\n // Pass 1: measure start beats + constant tempo regions from each `tempos[]`.\n // Gradual tempos are collected here and resolved into ramp regions below.\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n const nominalTime = playbackNominalTime(gm) ?? gm.time;\n if (nominalTime) currentTime = { count: nominalTime.count, unit: nominalTime.unit };\n const mBeats = measureBeatsFromTime(gm.time ?? currentTime);\n measureStartBeats.push(cumBeats);\n\n if (gm.tempos && gm.tempos.length > 0) {\n const sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));\n for (const tempo of sorted) {\n const startBeat = cumBeats + tempoBeat(tempo, mBeats);\n const qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);\n regions.push({ startBeat, endBeat: Infinity, startBpm: qpm, endBpm: qpm });\n }\n }\n if (gm.gradualTempo) {\n gradualDecls.push({\n declIdx: m,\n gt: gm.gradualTempo,\n startBeat: cumBeats + posBeats(gm.gradualTempo.position.fraction),\n });\n }\n cumBeats += mBeats;\n }\n\n // Default region from beat 0 if the score opens without a tempo.\n if (regions.length === 0 || regions.every((r) => r.startBeat > 1e-9)) {\n regions.unshift({ startBeat: 0, endBeat: Infinity, startBpm: DEFAULT_BPM, endBpm: DEFAULT_BPM });\n }\n\n // Resolve gradual-tempo ramps. `startBpm` defaults to the tempo active at the\n // ramp's start (read from a constant-only model so the ramp begins\n // continuously). A linear-in-BPM ramp region replaces the constant tempo over\n // [start, end); a trailing constant anchor at `end` holds `endBpm` afterwards\n // unless a real tempo (\"a tempo\") already starts there.\n if (gradualDecls.length > 0) {\n const idToIdx = buildMeasureIdToIndex(globalMeasures);\n const constModel = TempoModel.build(regions);\n for (const { declIdx, gt, startBeat } of gradualDecls) {\n const endBeat = resolveGradualEndBeat(gt.end, declIdx, globalMeasures, measureStartBeats, idToIdx);\n if (endBeat === undefined || endBeat <= startBeat + 1e-9) continue;\n const startBpm = gt.startBpm ?? constModel.bpmAtBeat(startBeat);\n regions.push({ startBeat, endBeat, startBpm, endBpm: gt.endBpm });\n const hasAnchorAtEnd = regions.some((r) => Math.abs(r.startBeat - endBeat) < 1e-9);\n if (!hasAnchorAtEnd) {\n regions.push({ startBeat: endBeat, endBeat: Infinity, startBpm: gt.endBpm, endBpm: gt.endBpm });\n }\n }\n }\n\n // Close each region at the next region's start (constant regions are\n // contiguous; ramp regions land exactly on the boundary we inserted at their\n // end; the final region stays open at Infinity).\n regions.sort((a, b) => a.startBeat - b.startBeat);\n for (let i = 0; i < regions.length - 1; i++) {\n regions[i]!.endBeat = regions[i + 1]!.startBeat;\n }\n\n // Insertions (fermata/caesura gaps). Seconds use the tempo at the measure\n // start (matching the legacy hold seconds), computed from a regions-only model.\n const regionsModel = TempoModel.build(regions);\n const insertions: TempoInsertion[] = [];\n if (holds) {\n for (let m = 0; m < holds.length; m++) {\n const measureHolds = holds[m];\n if (!measureHolds || measureHolds.length === 0) continue;\n const startBeat = measureStartBeats[m]!;\n const spb = regionsModel.spbAtBeat(startBeat);\n for (const hold of measureHolds) {\n insertions.push({ beat: startBeat + hold.atBeat, seconds: hold.extraBeats * spb });\n }\n }\n }\n\n const model = insertions.length > 0 ? TempoModel.build(regions, insertions) : regionsModel;\n const measureStartTimes = measureStartBeats.map((b) => model.timeAtBeat(b));\n return { model, measureStartTimes, measureStartBeats, totalBeats: cumBeats };\n}\n\n/** A rhythmic-position fraction (of a whole note) \u2192 quarter-note beats. */\nfunction posBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n/** Map every global-measure id \u2192 its index (for gradual-tempo end resolution). */\nfunction buildMeasureIdToIndex(globalMeasures: readonly GlobalMeasure[]): Map<string, number> {\n const m = new Map<string, number>();\n globalMeasures.forEach((gm, i) => {\n if (gm.id && !m.has(gm.id)) m.set(gm.id, i);\n });\n return m;\n}\n\n/** Resolve a gradual-tempo end (measure id + position) to a global beat. */\nfunction resolveGradualEndBeat(\n end: NonNullable<GlobalMeasure[\"gradualTempo\"]>[\"end\"],\n fromIdx: number,\n globalMeasures: readonly GlobalMeasure[],\n measureStartBeats: readonly number[],\n idToIdx: ReadonlyMap<string, number>,\n): number | undefined {\n let idx = idToIdx.get(end.measure);\n if (idx === undefined) {\n const parsed = Number.parseInt(end.measure, 10);\n if (!Number.isNaN(parsed) && parsed >= 0 && parsed < globalMeasures.length) idx = parsed;\n }\n if (idx === undefined) idx = fromIdx; // same-measure ramp fallback\n const start = measureStartBeats[idx];\n if (start === undefined) return undefined;\n return start + posBeats(end.position.fraction);\n}\n\n/** Global-measure tempo's beat offset within its measure (0 if unlocated). */\nfunction tempoBeat(tempo: NonNullable<GlobalMeasure[\"tempos\"]>[number], mBeats: number): number {\n return tempo.location ? (tempo.location.fraction[0] / tempo.location.fraction[1]) * mBeats : 0;\n}\n\n/**\n * Look up the seconds-per-quarter-note at a given absolute time.\n */\nexport function spqAtTime(tempoMap: readonly TempoMapEntry[], time: number): number {\n let activeQpm = DEFAULT_BPM;\n for (const entry of tempoMap) {\n if (entry.timeSeconds <= time + 1e-9) {\n activeQpm = entry.bpm;\n } else {\n break;\n }\n }\n return 60 / activeQpm;\n}\n", "/**\n * Tempo model \u2014 a continuous, sub-bar-resolved map between musical position\n * (global quarter-note beats) and wall-clock time (seconds).\n *\n * Why this exists: the original tempo handling resolved tempo only at measure\n * granularity (one BPM per bar, applied linearly), so a tempo change mid-bar,\n * a rit./accel., or a fermata-as-tempo-dip could not be expressed. This model\n * integrates a piecewise tempo curve at arbitrary beat resolution, which is\n * exactly what MIDI tempo meta-events express and what a studio click track\n * needs.\n *\n * The model is two layers:\n *\n * 1. **Tempo regions** \u2014 a sorted, contiguous list of `[startBeat, endBeat)`\n * spans, each with a start and end tempo (BPM of a quarter note). Within a\n * region the tempo varies LINEARLY IN BPM over beats: a constant region has\n * `startBpm === endBpm`; a rit./accel. is a ramp. Linear-in-BPM is the\n * conventional reading of a gradual tempo change and integrates to a clean,\n * invertible closed form (logarithmic time, exponential inverse).\n *\n * 2. **Point insertions** \u2014 instantaneous time gaps at a beat that DO NOT\n * advance musical position (a caesura / grand pause is pure silence). These\n * shift all later time without consuming beats.\n *\n * A fermata is modelled as a flat tempo DIP region (a constant region at a\n * slower tempo over the held span), NOT an insertion \u2014 so concurrent notes\n * underneath stretch correctly and following notes shift with zero \"bleed\".\n * (The fermata wiring lives in the hold layer; this module just integrates\n * whatever regions it is given.)\n */\n\n/** Seconds-per-beat \u2194 BPM helpers (quarter-note BPM). */\nconst bpmToSpb = (bpm: number): number => 60 / bpm;\n\n/** Slopes below this (BPM per beat) are treated as constant to avoid blowups. */\nconst RAMP_EPS = 1e-6;\n\n/** A contiguous tempo region in global quarter-note beats. */\nexport interface TempoRegion {\n /** Global quarter-note beat where this region starts (inclusive). */\n startBeat: number;\n /** Global quarter-note beat where this region ends (exclusive). The final\n * region uses `Infinity`. */\n endBeat: number;\n /** Quarter-note BPM at `startBeat`. */\n startBpm: number;\n /** Quarter-note BPM at `endBeat`. Equal to `startBpm` for a constant region;\n * different for a rit. (slower \u2192 smaller BPM) or accel. (faster). */\n endBpm: number;\n}\n\n/** An instantaneous time gap at a beat that does not advance musical position. */\nexport interface TempoInsertion {\n /** Global quarter-note beat at which the gap is inserted. */\n beat: number;\n /** Seconds of silence inserted. */\n seconds: number;\n}\n\n/** A precomputed knot: cumulative time at a region boundary (post-insertions). */\ninterface Knot {\n beat: number;\n /** Cumulative time at `beat`, just AFTER any insertion at this beat. */\n time: number;\n /** BPM at `beat` (region start tempo). */\n bpm: number;\n /** BPM slope (per beat) within the region starting here; 0 if constant. */\n slope: number;\n /** Seconds inserted exactly at this beat (point hold), already folded into\n * `time`; needed so the inverse maps the gap interval back to this beat. */\n insertBefore: number;\n}\n\n/**\n * Integrate the time advanced over `[b0, b]` within a single linear-BPM region.\n * Linear in BPM: `bpm(x) = bpm0 + slope\u00B7(x \u2212 b0)`. Time is `\u222B 60/bpm dx`:\n * - constant (slope\u22480): `60\u00B7(b\u2212b0)/bpm0`\n * - ramp: `(60/slope)\u00B7ln(bpm(b)/bpm0)`\n */\nfunction integrateTime(b0: number, b: number, bpm0: number, slope: number): number {\n const db = b - b0;\n if (Math.abs(slope) < RAMP_EPS) return bpmToSpb(bpm0) * db;\n const bpmB = bpm0 + slope * db;\n return (60 / slope) * Math.log(bpmB / bpm0);\n}\n\n/**\n * Invert {@link integrateTime}: given elapsed seconds `dt` from `b0`, return the\n * beat reached. The inverse of the closed forms above.\n */\nfunction inverseBeat(dt: number, bpm0: number, slope: number): number {\n if (Math.abs(slope) < RAMP_EPS) return (dt * bpm0) / 60;\n // dt = (60/slope)\u00B7ln(bpm/bpm0) \u2192 bpm = bpm0\u00B7exp(dt\u00B7slope/60); db = (bpm\u2212bpm0)/slope\n const bpmAt = bpm0 * Math.exp((dt * slope) / 60);\n return (bpmAt - bpm0) / slope;\n}\n\n/**\n * A continuous tempo map. Construct via {@link TempoModel.build}.\n *\n * All public queries are O(log n) over the region/knot list.\n */\nexport class TempoModel {\n private readonly knots: readonly Knot[];\n\n private constructor(knots: readonly Knot[]) {\n this.knots = knots;\n }\n\n /**\n * Build a model from tempo regions and point insertions.\n *\n * `regions` need not be pre-sorted or gapless; they are sorted by `startBeat`\n * and gaps are filled by extending the previous tempo (a held constant). The\n * first region's start tempo applies from beat 0. `insertions` are point gaps\n * (caesuras) keyed by beat.\n */\n static build(regions: readonly TempoRegion[], insertions: readonly TempoInsertion[] = []): TempoModel {\n const sorted = [...regions].sort((a, b) => a.startBeat - b.startBeat);\n const insByBeat = new Map<number, number>();\n for (const ins of insertions) {\n if (ins.seconds > 0) insByBeat.set(ins.beat, (insByBeat.get(ins.beat) ?? 0) + ins.seconds);\n }\n\n // Collect boundary beats: every region start + every insertion beat.\n const boundarySet = new Set<number>([0]);\n for (const r of sorted) boundarySet.add(r.startBeat);\n for (const ins of insByBeat.keys()) boundarySet.add(ins);\n const boundaries = [...boundarySet].sort((a, b) => a - b);\n\n // Resolve the tempo (bpm + slope) active at a given beat from the regions.\n const tempoAt = (beat: number): { bpm: number; slope: number } => {\n let active: TempoRegion | undefined;\n for (const r of sorted) {\n if (r.startBeat <= beat + 1e-9) active = r;\n else break;\n }\n if (!active) {\n // Before the first region: use the first region's start tempo, or 120.\n const first = sorted[0];\n return { bpm: first ? first.startBpm : 120, slope: 0 };\n }\n const span = active.endBeat - active.startBeat;\n const slope = span > 0 && Number.isFinite(span) ? (active.endBpm - active.startBpm) / span : 0;\n const bpmAtBeat = active.startBpm + slope * (beat - active.startBeat);\n return { bpm: bpmAtBeat, slope };\n };\n\n const knots: Knot[] = [];\n let time = 0;\n let prevBeat = boundaries[0]!;\n let prev = tempoAt(prevBeat);\n // First knot (with any insertion at beat 0).\n const ins0 = insByBeat.get(prevBeat) ?? 0;\n time += ins0;\n knots.push({ beat: prevBeat, time, bpm: prev.bpm, slope: prev.slope, insertBefore: ins0 });\n\n for (let i = 1; i < boundaries.length; i++) {\n const beat = boundaries[i]!;\n // Advance time across [prevBeat, beat) using the tempo active at prevBeat.\n time += integrateTime(prevBeat, beat, prev.bpm, prev.slope);\n const ins = insByBeat.get(beat) ?? 0;\n time += ins;\n const t = tempoAt(beat);\n knots.push({ beat, time, bpm: t.bpm, slope: t.slope, insertBefore: ins });\n prevBeat = beat;\n prev = t;\n }\n\n return new TempoModel(knots);\n }\n\n /** Index of the last knot whose beat \u2264 `beat`. */\n private knotIndexForBeat(beat: number): number {\n const k = this.knots;\n let lo = 0;\n let hi = k.length - 1;\n let ans = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (k[mid]!.beat <= beat + 1e-9) {\n ans = mid;\n lo = mid + 1;\n } else hi = mid - 1;\n }\n return ans;\n }\n\n /** Index of the last knot whose time \u2264 `time`. */\n private knotIndexForTime(time: number): number {\n const k = this.knots;\n let lo = 0;\n let hi = k.length - 1;\n let ans = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (k[mid]!.time <= time + 1e-9) {\n ans = mid;\n lo = mid + 1;\n } else hi = mid - 1;\n }\n return ans;\n }\n\n /**\n * Absolute time (seconds) at a global quarter-note beat.\n *\n * `endpoint` selects how a point insertion EXACTLY at `beat` is treated:\n * - `\"onset\"` (default): time AFTER the insertion \u2014 what an event STARTING at\n * `beat` wants (it begins once the gap is over).\n * - `\"release\"`: time BEFORE the insertion \u2014 what a note ENDING at `beat`\n * wants (a caesura/gap at the release point isn't part of the note).\n */\n timeAtBeat(beat: number, endpoint: \"onset\" | \"release\" = \"onset\"): number {\n const i = this.knotIndexForBeat(beat);\n const knot = this.knots[i]!;\n const base = knot.time + integrateTime(knot.beat, beat, knot.bpm, knot.slope);\n if (endpoint === \"release\" && Math.abs(knot.beat - beat) < 1e-9) {\n // `knot.time` already folds in `insertBefore`; back it out for a release.\n return base - knot.insertBefore;\n }\n return base;\n }\n\n /**\n * Seconds elapsed over `[beat, beat + beats)` \u2014 a note/event DURATION. The end\n * uses release semantics so a point insertion (caesura) exactly at the release\n * beat is not absorbed into the note's sounding length.\n */\n secondsForBeats(beat: number, beats: number): number {\n return this.timeAtBeat(beat + beats, \"release\") - this.timeAtBeat(beat);\n }\n\n /**\n * Global quarter-note beat at an absolute time (inverse of {@link timeAtBeat}).\n * Within a point-insertion gap, returns the insertion's beat (position is\n * frozen during a caesura).\n */\n beatAtTime(time: number): number {\n const i = this.knotIndexForTime(time);\n const knot = this.knots[i]!;\n const next = this.knots[i + 1];\n if (next) {\n // The music for this knot runs until just before the NEXT knot's point\n // insertion; the interval [musicEnd, next.time) is that insertion's gap,\n // during which position is frozen at next.beat (a caesura holds time).\n const musicEnd = next.time - next.insertBefore;\n if (time >= musicEnd - 1e-9) return next.beat;\n }\n return knot.beat + inverseBeat(time - knot.time, knot.bpm, knot.slope);\n }\n\n /** Seconds-per-quarter-note beat at a global beat. */\n spbAtBeat(beat: number): number {\n const i = this.knotIndexForBeat(beat);\n const knot = this.knots[i]!;\n const bpmHere = knot.bpm + knot.slope * (beat - knot.beat);\n return bpmToSpb(bpmHere);\n }\n\n /** Quarter-note BPM at a global beat. */\n bpmAtBeat(beat: number): number {\n return 60 / this.spbAtBeat(beat);\n }\n\n /**\n * Generate click times (seconds) for a metronome / studio click track over\n * `[startBeat, endBeat)`. `clickBeats` are the beat positions to click,\n * expressed as offsets within each \"grid step\" \u2014 but for the common case\n * callers pass a flat list of global beat positions and we just map them.\n * Returns the absolute time of every beat in `beats`.\n */\n clickTimes(beats: readonly number[]): number[] {\n return beats.map((b) => this.timeAtBeat(b));\n }\n}\n", "/**\n * Dynamics envelope \u2014 couples velocity and CC11 (expression) so a single\n * notated dynamic level drives BOTH axes from one source of truth.\n *\n * Why two axes at all:\n * - Velocity selects the SF2 sample layer (timbre) and the attack level. It is\n * fixed at noteOn and cannot change while a note sustains.\n * - CC11 (expression) is a per-voice amplitude scalar that DOES respond during\n * a held note, so it carries the level and enables long-note shaping\n * (hairpins, messa di voce).\n *\n * Perceived loudness is roughly `velGain(velocity) \u00D7 CC11/127` \u2014 a PRODUCT. If\n * the dynamic level were encoded independently into both axes they would\n * \"stack\" (f\u00B2 too loud, pp\u00B2 inaudible). The fix here is a single mapping\n * `dynamic \u2192 {velocity, cc11}` (`DYNAMIC_AXES`): both axes derive from the same\n * level, so the combined curve is designed, not accidental. Each axis spans a\n * compressed range (velocity ~19 dB, cc11 ~11 dB) so the PRODUCT lands on a\n * musical ~30 dB orchestral range.\n *\n * The envelope models three things over a part's timeline:\n * - ANCHORS: explicit graded dynamics (p, f, \u2026) \u2014 a step function.\n * - RAMPS: hairpins (cresc./dim.) \u2014 both axes interpolate CONTINUOUSLY across\n * the span (velocity per note = anti-zipper; CC11 on a fine grid = smooth\n * swell), so a multi-measure cresc over mixed short/long notes shapes\n * correctly and a held note gets a real messa di voce. Consecutive hairpins\n * chain (a dim. starting where a cresc. ends inherits its peak level), and an\n * OPEN hairpin (no written target) moves one dynamic step in its direction.\n * - ATTACKS: accent groups alter the note-on velocity at their onset without\n * swelling unrelated held notes. An accent's `residualValue` additionally\n * drops the persistent level, which is how `fp` is encoded.\n *\n * CC11 is owned exclusively by this system (the mute keyswitch is timbre-only \u2014\n * CC74/71 \u2014 and no longer touches CC11). CC7 (channel volume) belongs to the\n * mixer and is never written here.\n */\n\nimport {\n walkSequenceEvents,\n type DynamicGroup,\n type DynamicValue,\n type GlobalMeasure,\n type GradualDynamicGroup,\n type Part,\n} from \"@viritura/core\";\nimport type { ImpliedSectionDynamicAnchor } from \"./sectionDynamics\";\nimport type { MidiEvent } from \"./types\";\nimport type { TempoModel } from \"./tempoModel\";\nimport { resolvePartStaffMeterTable, staffMeterRatioAt, type StaffMeterTable } from \"./staffMeterTiming\";\n\n/** CC 11 (expression). The coupled dynamics system is its sole owner. */\nconst CC_EXPRESSION = 11;\n\n/** Time-coincidence tolerance (seconds). */\nconst EPS = 1e-6;\n\n/** The two coupled MIDI axes a dynamic level maps to. */\nexport interface DynamicAxes {\n /** noteOn velocity (1\u2013127): selects sample layer + attack level. */\n velocity: number;\n /** CC11 expression (0\u2013127): sustained level, shapeable mid-note. */\n cc11: number;\n}\n\n/** Neutral default (mf) when no dynamic marking precedes a note. */\nexport const DEFAULT_DYNAMIC: DynamicAxes = { velocity: 84, cc11: 100 };\n\n/**\n * Coupled velocity/CC11 calibration per dynamic marking.\n *\n * Velocity spans 30\u2192127, CC11 spans 18\u2192127; the product targets ~30 dB. These\n * are tuning constants \u2014 adjust by ear, keeping both axes monotonic with level.\n *\n * The score stores semantic values; these are the generic fallback realization\n * for an SF2/MIDI backend. Instrument-specific response profiles can project\n * the same semantic scale differently.\n */\nexport const DYNAMIC_AXES: Record<DynamicValue, DynamicAxes> = {\n n: { velocity: 1, cc11: 0 },\n pppppp: { velocity: 22, cc11: 11 },\n ppppp: { velocity: 28, cc11: 17 },\n pppp: { velocity: 34, cc11: 24 },\n ppp: { velocity: 42, cc11: 34 },\n pp: { velocity: 52, cc11: 50 },\n p: { velocity: 64, cc11: 66 },\n mp: { velocity: 74, cc11: 82 },\n mf: { velocity: 84, cc11: 100 },\n f: { velocity: 98, cc11: 112 },\n ff: { velocity: 112, cc11: 122 },\n fff: { velocity: 122, cc11: 127 },\n // CC11 saturates at fff, so the extreme fortissimos separate on velocity\n // alone \u2014 which is also what selects the loudest SF2 sample layers.\n ffff: { velocity: 124, cc11: 127 },\n fffff: { velocity: 126, cc11: 127 },\n ffffff: { velocity: 127, cc11: 127 },\n};\n\n/** Look up the coupled axes for a dynamic marking, or undefined if unknown. */\nexport function dynamicToAxes(value: DynamicValue): DynamicAxes {\n return DYNAMIC_AXES[value];\n}\n\n/** Axes for a standard dynamic value. */\nfunction axesOf(value: DynamicValue): DynamicAxes {\n return DYNAMIC_AXES[value];\n}\n\n/** Graded dynamic ladder (soft \u2192 loud) for hairpin fallback stepping. */\nconst DYNAMIC_LADDER: readonly DynamicValue[] = [\n \"n\",\n \"pppppp\",\n \"ppppp\",\n \"pppp\",\n \"ppp\",\n \"pp\",\n \"p\",\n \"mp\",\n \"mf\",\n \"f\",\n \"ff\",\n \"fff\",\n \"ffff\",\n \"fffff\",\n \"ffffff\",\n];\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Envelope model\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** An explicit graded dynamic at an absolute time (step). */\ninterface DynamicAnchor {\n time: number;\n velocity: number;\n cc11: number;\n}\n\n/** A hairpin span: both axes interpolate from `start` to `end` over the span. */\ninterface DynamicRamp {\n groupId: string;\n startTime: number;\n endTime: number;\n start: DynamicAxes;\n end: DynamicAxes;\n}\n\n/** A dynamic attack override at one onset. */\ninterface DynamicAttack {\n time: number;\n attackVelocity: number;\n}\n\n/** A part's coupled dynamics over time. */\nexport interface DynamicsEnvelope {\n anchors: readonly DynamicAnchor[];\n ramps: readonly DynamicRamp[];\n attacks: readonly DynamicAttack[];\n}\n\n/** Convert a measure-relative fraction (of a whole note) to quarter-note beats. */\nfunction fractionToBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n/** Clamp to [0,1]. */\nfunction clamp01(x: number): number {\n return x < 0 ? 0 : x > 1 ? 1 : x;\n}\n\n/** Linear blend of two axes at fraction f (rounded to ints). */\nfunction lerpAxes(a: DynamicAxes, b: DynamicAxes, frac: number): DynamicAxes {\n const f = clamp01(frac);\n return {\n velocity: Math.round(a.velocity + (b.velocity - a.velocity) * f),\n cc11: Math.round(a.cc11 + (b.cc11 - a.cc11) * f),\n };\n}\n\n/** Nearest ladder index to a given level (by velocity). */\nfunction nearestLadderIndex(axes: DynamicAxes): number {\n let best = 0;\n let bestDist = Infinity;\n for (let i = 0; i < DYNAMIC_LADDER.length; i++) {\n const dist = Math.abs(DYNAMIC_AXES[DYNAMIC_LADDER[i]!]!.velocity - axes.velocity);\n if (dist < bestDist) {\n bestDist = dist;\n best = i;\n }\n }\n return best;\n}\n\n/** Step one rung along the ladder in `dir` (+1 louder, \u22121 softer). */\nfunction stepRung(axes: DynamicAxes, dir: 1 | -1): DynamicAxes {\n const i = nearestLadderIndex(axes);\n const j = Math.max(0, Math.min(DYNAMIC_LADDER.length - 1, i + dir));\n return DYNAMIC_AXES[DYNAMIC_LADDER[j]!]!;\n}\n\n/** Sample only the anchor step function (no ramps) at time `t`. */\nfunction sampleAnchorsAxes(anchors: readonly DynamicAnchor[], t: number): DynamicAxes {\n let active: DynamicAxes = DEFAULT_DYNAMIC;\n for (const a of anchors) {\n if (a.time > t + EPS) break;\n active = { velocity: a.velocity, cc11: a.cc11 };\n }\n return active;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Build\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Map every global measure id \u2192 its original index (for hairpin end lookup). */\nfunction buildMeasureIdToOrig(globalMeasures: readonly GlobalMeasure[]): Map<string, number> {\n const m = new Map<string, number>();\n globalMeasures.forEach((gm, i) => {\n if (gm.id) m.set(gm.id, i);\n });\n return m;\n}\n\ninterface TimedDynamicGroup {\n group: DynamicGroup;\n time: number;\n expandedIdx: number;\n}\n\ninterface TimedGradualText {\n id: string;\n startTime: number;\n dir: 1 | -1;\n expandedIdx: number;\n}\n\nfunction gradualTextDirection(text: string): 1 | -1 | undefined {\n const normalized = text.trim().toLocaleLowerCase();\n if (/^cresc(?:endo)?\\.?$/.test(normalized)) return 1;\n if (/^(?:dim(?:inuendo)?|decresc(?:endo)?)\\.?$/.test(normalized)) return -1;\n return undefined;\n}\n\nfunction collectMeasureDynamicGroups(\n pm: Part[\"measures\"][number],\n originalMeasureIndex: number,\n measureStartBeat: number,\n model: TempoModel,\n expandedIdx: number,\n groups: TimedDynamicGroup[],\n staffMeterTable: StaffMeterTable,\n): void {\n for (const group of pm.dynamics ?? []) {\n const ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff);\n groups.push({\n group,\n time: model.timeAtBeat(measureStartBeat + fractionToBeats(group.position.fraction) * ratio),\n expandedIdx,\n });\n }\n}\n\nfunction collectMeasureGradualTexts(\n pm: Part[\"measures\"][number],\n originalMeasureIndex: number,\n measureStartBeat: number,\n model: TempoModel,\n expandedIdx: number,\n gradualTexts: TimedGradualText[],\n staffMeterTable: StaffMeterTable,\n): void {\n for (let index = 0; index < (pm.expressions?.length ?? 0); index++) {\n const expression = pm.expressions![index]!;\n const dir = gradualTextDirection(expression.text);\n if (!dir) continue;\n gradualTexts.push({\n id: `text-gradual-${expandedIdx}-${index}`,\n startTime: model.timeAtBeat(\n measureStartBeat +\n fractionToBeats(expression.position.fraction) *\n staffMeterRatioAt(staffMeterTable, originalMeasureIndex, expression.staff),\n ),\n dir,\n expandedIdx,\n });\n }\n}\n\n/** Resolve a hairpin's end-measure start beat (nearest forward occurrence). */\nfunction resolveEndMeasureLocation(\n endMeasureId: string,\n fromExpandedIdx: number,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n idToOrig: ReadonlyMap<string, number>,\n): { measureStartBeat: number; originalMeasureIndex: number } | undefined {\n let targetOrig = idToOrig.get(endMeasureId);\n if (targetOrig === undefined) {\n // Fallback: some scores use the numeric measure index as the id.\n const parsed = Number.parseInt(endMeasureId, 10);\n if (!Number.isNaN(parsed)) targetOrig = parsed;\n }\n if (targetOrig === undefined) return undefined;\n for (let j = fromExpandedIdx; j < measureOrder.length; j++) {\n if (measureOrder[j] === targetOrig) {\n return { measureStartBeat: measureStartBeats[j]!, originalMeasureIndex: targetOrig };\n }\n }\n for (let j = 0; j < measureOrder.length; j++) {\n if (measureOrder[j] === targetOrig) {\n return { measureStartBeat: measureStartBeats[j]!, originalMeasureIndex: targetOrig };\n }\n }\n return undefined;\n}\n\n/** Axes of an explicit (authored) dynamic anchor at (\u2248) `time`, or undefined. */\nfunction explicitAnchorAt(anchors: readonly DynamicAnchor[], time: number): DynamicAxes | undefined {\n for (const a of anchors) {\n if (Math.abs(a.time - time) <= EPS) return { velocity: a.velocity, cc11: a.cc11 };\n }\n return undefined;\n}\n\n/** A hairpin with resolved absolute start/end times and direction. */\ninterface TimedHairpin {\n groupId: string;\n startTime: number;\n endTime: number;\n dir: 1 | -1;\n start?: DynamicAxes;\n startsAfterSilence: boolean;\n measureStartTime: number;\n}\n\nfunction measureHasSoundingNotes(part: Part, measureIndex: number | undefined): boolean {\n if (measureIndex === undefined) return false;\n return (part.measures[measureIndex]?.sequences ?? []).some((sequence) =>\n [...walkSequenceEvents(sequence.content)].some(({ event }) => !!event.notes?.length || !!event.kitNotes?.length),\n );\n}\n\nfunction startsAfterSilentMeasure(part: Part, measureOrder: readonly number[], expandedIdx: number): boolean {\n return expandedIdx > 0 && !measureHasSoundingNotes(part, measureOrder[expandedIdx - 1]);\n}\n\n/**\n * Resolve timed hairpins to ramps in time order, CHAINING consecutive ones:\n * when a hairpin begins exactly where the previous one ended (and no explicit\n * dynamic is written there), it inherits the previous ramp's end level as its\n * start. This makes a messa di voce (a crescendo and decrescendo meeting at a\n * peak) connect smoothly instead of snapping back to the standing level at the\n * seam.\n *\n * Endpoint rules:\n * - START: an explicit dynamic written at the start wins; else the chained\n * previous end (if the hairpins meet); else the standing (sampled) level.\n * - END: an explicit dynamic written at the end wins; else \u2014 for an OPEN\n * hairpin with no written target \u2014 step ONE ladder rung in the hairpin's\n * direction (cresc. \u2192 one louder, dim. \u2192 one softer). So a chained\n * mf-crescendo\u2192f then open decrescendo lands back on mf (f one rung down),\n * giving a symmetric swell.\n */\nfunction resolveRamps(timed: readonly TimedHairpin[], anchors: readonly DynamicAnchor[]): DynamicRamp[] {\n const ordered = [...timed].sort((a, b) => a.startTime - b.startTime);\n const ramps: DynamicRamp[] = [];\n let prev: DynamicRamp | undefined;\n for (const {\n groupId,\n startTime,\n endTime,\n dir,\n start: authoredStart,\n startsAfterSilence,\n measureStartTime,\n } of ordered) {\n if (endTime <= startTime + EPS) continue;\n const chained = prev !== undefined && Math.abs(prev.endTime - startTime) <= EPS;\n const currentMeasureAnchor = anchors.some(\n (anchor) => anchor.time >= measureStartTime - EPS && anchor.time <= startTime + EPS,\n );\n const start =\n authoredStart ??\n explicitAnchorAt(anchors, startTime) ??\n (startsAfterSilence && !currentMeasureAnchor\n ? axesOf(\"p\")\n : chained\n ? prev!.end\n : sampleAnchorsAxes(anchors, startTime));\n const end = explicitAnchorAt(anchors, endTime) ?? stepRung(start, dir);\n const ramp: DynamicRamp = { groupId, startTime, endTime, start, end };\n ramps.push(ramp);\n prev = ramp;\n }\n return ramps;\n}\n\nfunction collectExpandedDynamics(\n part: Part,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n staffMeterTable: StaffMeterTable,\n timedGroups: TimedDynamicGroup[],\n gradualTexts: TimedGradualText[],\n): void {\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const originalMeasureIndex = measureOrder[expandedIdx]!;\n const measure = part.measures[originalMeasureIndex];\n if (!measure) continue;\n const measureStartBeat = measureStartBeats[expandedIdx]!;\n collectMeasureDynamicGroups(\n measure,\n originalMeasureIndex,\n measureStartBeat,\n model,\n expandedIdx,\n timedGroups,\n staffMeterTable,\n );\n collectMeasureGradualTexts(\n measure,\n originalMeasureIndex,\n measureStartBeat,\n model,\n expandedIdx,\n gradualTexts,\n staffMeterTable,\n );\n }\n}\n\n/**\n * Build a part's dynamics envelope from standard dynamic groups. Walks the\n * expanded measure order, so repeated material yields one instance per pass.\n */\nexport function buildDynamicsEnvelope(\n part: Part,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n globalMeasures: readonly GlobalMeasure[],\n impliedAnchors: readonly ImpliedSectionDynamicAnchor[] = [],\n resolvedStaffMeters?: StaffMeterTable,\n): DynamicsEnvelope {\n const anchors: DynamicAnchor[] = [];\n const attacks: DynamicAttack[] = [];\n const timedGroups: TimedDynamicGroup[] = [];\n const gradualTexts: TimedGradualText[] = [];\n const idToOrig = buildMeasureIdToOrig(globalMeasures);\n const staffMeterTable = resolvedStaffMeters ?? resolvePartStaffMeterTable(part, globalMeasures);\n\n // Pass 1: collect and order every dynamic group on the expanded timeline.\n collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts);\n const typeRank: Record<DynamicGroup[\"type\"], number> = { immediate: 0, relative: 1, accent: 2, gradual: 3 };\n timedGroups.sort(\n (a, b) =>\n a.time - b.time || typeRank[a.group.type] - typeRank[b.group.type] || a.group.id.localeCompare(b.group.id),\n );\n\n // Pass 2: resolve persistent levels and onset attacks.\n for (const { group, time } of timedGroups) {\n if (group.type === \"immediate\") {\n const body = axesOf(group.value);\n anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });\n } else if (group.type === \"relative\") {\n const current = sampleAnchorsAxes(anchors, time - EPS);\n const next = stepRung(current, group.relativeValue === \"louder\" ? 1 : -1);\n anchors.push({ time, velocity: next.velocity, cc11: next.cc11 });\n } else if (group.type === \"accent\") {\n // `value` is the attack; `residualValue` (the \"p\" of \"fp\") is the level\n // that persists from this onset onward.\n attacks.push({ time, attackVelocity: axesOf(group.value).velocity });\n if (group.residualValue !== undefined) {\n const body = axesOf(group.residualValue);\n anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });\n }\n }\n }\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const measureIndex = measureOrder[expandedIdx]!;\n for (const implied of impliedAnchors) {\n if (implied.measureIndex !== measureIndex) continue;\n const time = model.timeAtBeat(measureStartBeats[expandedIdx]! + fractionToBeats(implied.position));\n if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS)) continue;\n const axes = axesOf(implied.value);\n anchors.push({ time, velocity: axes.velocity, cc11: axes.cc11 });\n }\n }\n anchors.sort((a, b) => a.time - b.time);\n\n // Pass 3: resolve gradual end times, then chain them into ramps.\n const timed: TimedHairpin[] = [];\n for (const { group, time: startTime, expandedIdx } of timedGroups) {\n if (group.type !== \"gradual\") continue;\n const gradual: GradualDynamicGroup = group;\n const endMeasure = resolveEndMeasureLocation(\n gradual.end.measure,\n expandedIdx,\n measureOrder,\n measureStartBeats,\n idToOrig,\n );\n if (endMeasure === undefined) continue;\n const endRatio = staffMeterRatioAt(\n staffMeterTable,\n endMeasure.originalMeasureIndex,\n gradual.staffEnd ?? gradual.staff,\n );\n const endTime = model.timeAtBeat(\n endMeasure.measureStartBeat + fractionToBeats(gradual.end.position.fraction) * endRatio,\n );\n timed.push({\n groupId: gradual.id,\n startTime,\n endTime,\n dir: gradual.wedgeType === \"increasing\" ? 1 : -1,\n start: gradual.value === undefined ? undefined : axesOf(gradual.value),\n startsAfterSilence: startsAfterSilentMeasure(part, measureOrder, expandedIdx),\n measureStartTime: model.timeAtBeat(measureStartBeats[expandedIdx]!),\n });\n }\n for (const text of gradualTexts) {\n if (timed.some((hairpin) => Math.abs(hairpin.startTime - text.startTime) <= EPS)) continue;\n const endpoint = anchors.find((anchor) => anchor.time > text.startTime + EPS);\n if (!endpoint) continue;\n timed.push({\n groupId: text.id,\n startTime: text.startTime,\n endTime: endpoint.time,\n dir: text.dir,\n startsAfterSilence: startsAfterSilentMeasure(part, measureOrder, text.expandedIdx),\n measureStartTime: model.timeAtBeat(measureStartBeats[text.expandedIdx]!),\n });\n }\n const ramps = resolveRamps(timed, anchors);\n attacks.sort((a, b) => a.time - b.time);\n\n return { anchors, ramps, attacks };\n}\n\n/** Whether the part declares any dynamics at all. */\nexport function hasAnyDynamics(env: DynamicsEnvelope): boolean {\n return env.anchors.length > 0 || env.ramps.length > 0 || env.attacks.length > 0;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sampling\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** The ramp covering time `t`, if any. */\nfunction rampAt(env: DynamicsEnvelope, t: number): DynamicRamp | undefined {\n for (const r of env.ramps) {\n if (t >= r.startTime - EPS && t <= r.endTime + EPS) return r;\n }\n return undefined;\n}\n\n/**\n * Sample the coupled axes (velocity + cc11) at absolute time `t`. A hairpin ramp\n * interpolates continuously across its span; elsewhere the anchor step holds.\n * Attacks do not alter the sampled standing level; see `noteVelocityAt`.\n */\nexport function sampleDynamics(env: DynamicsEnvelope, t: number): DynamicAxes {\n const r = rampAt(env, t);\n if (r) {\n const frac = (t - r.startTime) / (r.endTime - r.startTime);\n return lerpAxes(r.start, r.end, frac);\n }\n return sampleAnchorsAxes(env.anchors, t);\n}\n\n/** An attack override at (approximately) time `t`, if any. */\nfunction attackAt(env: DynamicsEnvelope, t: number): DynamicAttack | undefined {\n for (const c of env.attacks) {\n if (Math.abs(c.time - t) <= EPS) return c;\n }\n return undefined;\n}\n\n/**\n * noteOn velocity at an onset: an authored attack velocity\n * overrides the sampled level; otherwise the sampled (possibly ramped) velocity.\n */\nexport function noteVelocityAt(env: DynamicsEnvelope, t: number): number {\n const c = attackAt(env, t);\n return c ? c.attackVelocity : sampleDynamics(env, t).velocity;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// CC11 emission\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport interface Cc11Event {\n time: number;\n value: number;\n}\n\n/** Exact events where a linear ramp crosses a rounded 7-bit CC11 boundary. */\nfunction rampQuantizationEvents(ramp: DynamicRamp): Cc11Event[] {\n const start = ramp.start.cc11;\n const end = ramp.end.cc11;\n if (start === end || ramp.endTime <= ramp.startTime + EPS) {\n return [\n { time: ramp.startTime, value: start },\n { time: ramp.endTime, value: end },\n ];\n }\n const direction = end > start ? 1 : -1;\n const events: Cc11Event[] = [{ time: ramp.startTime, value: start }];\n for (let value = start + direction; direction > 0 ? value <= end : value >= end; value += direction) {\n const boundary = value - direction * 0.5;\n const fraction = (boundary - start) / (end - start);\n events.push({\n time: ramp.startTime + fraction * (ramp.endTime - ramp.startTime),\n value,\n });\n }\n events.push({ time: ramp.endTime, value: end });\n return events;\n}\n\n/**\n * Resolve the ordered CC11 events for a part's envelope: a baseline at part\n * start, anchors, and exact rounded-value crossings. Consecutive equal values\n * are deduped, so a monotonic ramp emits no more than 127 useful writes.\n */\nexport function cc11Events(env: DynamicsEnvelope): Cc11Event[] {\n const raw: Cc11Event[] = [{ time: 0, value: sampleDynamics(env, 0).cc11 }];\n for (const anchor of env.anchors) raw.push({ time: anchor.time, value: anchor.cc11 });\n for (const ramp of env.ramps) raw.push(...rampQuantizationEvents(ramp));\n raw.sort((a, b) => a.time - b.time);\n\n const out: Cc11Event[] = [];\n for (const e of raw) {\n const prev = out[out.length - 1];\n if (prev && Math.abs(prev.time - e.time) <= EPS) {\n out[out.length - 1] = e; // same instant: later write wins\n continue;\n }\n if (prev && prev.value === e.value) continue; // unchanged level\n out.push(e);\n }\n if (out.length === 0 || out[0]!.time > EPS) {\n out.unshift({ time: 0, value: DEFAULT_DYNAMIC.cc11 });\n }\n return out;\n}\n\n/**\n * Append a part's coupled-dynamics CC11 (expression) MIDI events: a baseline at\n * part start plus one event per level change. CC11 is the sole dynamics level\n * axis (the mute keyswitch is timbre-only). The global sort places controlChange\n * before any noteOn at the same time.\n */\nexport function emitDynamicsCc11(\n env: DynamicsEnvelope,\n partIndex: number,\n channel: number,\n out: MidiEvent[],\n playbackLaneId?: string,\n): void {\n for (const ev of cc11Events(env)) {\n out.push({\n type: \"controlChange\",\n time: ev.time,\n midiNote: 0,\n velocity: 0,\n partIndex,\n playbackLaneId,\n channel,\n cc: CC_EXPRESSION,\n value: ev.value,\n });\n }\n}\n", "export { compileDynamicProgram, laneForScope, laneForSequence, playbackLaneId } from \"./dynamicProgram\";\nexport { realizeDynamicsEnvelope, selectDynamicResponseProfile } from \"./dynamicRealization\";\nexport type {\n DynamicPlaybackDiagnostic,\n DynamicProgram,\n DynamicResponseProfile,\n PlaybackLane,\n PlaybackLaneId,\n} from \"./types\";\n", "import type { DynamicGroup, GlobalMeasure, Part, Sequence, TextExpression } from \"@viritura/core\";\nimport { buildDynamicsEnvelope } from \"../dynamicsEnvelope\";\nimport type { TempoModel } from \"../tempoModel\";\nimport { realizeDynamicsEnvelope, selectDynamicResponseProfile } from \"./dynamicRealization\";\nimport type { DynamicPlaybackDiagnostic, DynamicProgram, PlaybackLane, PlaybackLaneId } from \"./types\";\nimport type { ImpliedSectionDynamicAnchor } from \"../sectionDynamics\";\nimport type { StaffMeterTable } from \"../staffMeterTiming\";\n\ninterface LaneScope {\n staff: number;\n voice: string;\n}\n\nfunction sequenceVoice(sequence: Sequence, index: number): string {\n return sequence.voice ?? `sequence:${index}`;\n}\n\nfunction collectContentStaves(content: Readonly<Sequence[\"content\"]>, staves: Set<number>): void {\n for (const item of content) {\n if (item.type === \"event\") {\n if (item.staff !== undefined) staves.add(item.staff);\n } else if (item.type === \"tuplet\") {\n collectContentStaves(item.content, staves);\n } else if (item.type === \"grace\" || item.type === \"tremolo\") {\n for (const event of item.content) {\n if (event.staff !== undefined) staves.add(event.staff);\n }\n }\n }\n}\n\nexport function playbackLaneId(partIndex: number, staff?: number, voice?: string): PlaybackLaneId {\n return staff === undefined || voice === undefined\n ? `part:${partIndex}`\n : `part:${partIndex}:staff:${staff}:voice:${voice}`;\n}\n\nfunction collectLaneScopes(part: Part): LaneScope[] {\n const byId = new Map<string, LaneScope>();\n for (const measure of part.measures) {\n measure.sequences.forEach((sequence, index) => {\n const voice = sequenceVoice(sequence, index);\n const staves = new Set([sequence.staff ?? 1]);\n collectContentStaves(sequence.content, staves);\n for (const staff of staves) {\n const scope = { staff, voice };\n byId.set(`${scope.staff}\\u0000${scope.voice}`, scope);\n }\n });\n }\n return byId.size > 0 ? [...byId.values()] : [{ staff: 1, voice: \"sequence:0\" }];\n}\n\nfunction appliesToLane(group: DynamicGroup | TextExpression, scope: LaneScope): boolean {\n return (\n (group.staff === undefined || group.staff === scope.staff) &&\n (group.voice === undefined || group.voice === scope.voice)\n );\n}\n\nfunction withLaneDynamics(part: Part, scope: LaneScope): Part {\n return {\n ...part,\n measures: part.measures.map((measure) => ({\n ...measure,\n dynamics: measure.dynamics?.filter((group) => appliesToLane(group, scope)),\n expressions: measure.expressions?.filter((expression) => appliesToLane(expression, scope)),\n })),\n };\n}\n\nfunction collectScopeDiagnostics(part: Part, scopes: readonly LaneScope[]): DynamicPlaybackDiagnostic[] {\n const diagnostics: DynamicPlaybackDiagnostic[] = [];\n const voices = new Set(scopes.map((scope) => scope.voice));\n const reportedMissing = new Set<string>();\n for (const measure of part.measures) {\n for (const group of measure.dynamics ?? []) {\n if (group.voice && !voices.has(group.voice) && !reportedMissing.has(group.id)) {\n reportedMissing.add(group.id);\n diagnostics.push({\n code: \"missing-voice\",\n message: `Dynamic group ${group.id} targets voice ${group.voice}, which does not exist in the part.`,\n groupIds: [group.id],\n });\n }\n }\n const groups = measure.dynamics ?? [];\n for (let i = 0; i < groups.length; i++) {\n const left = groups[i]!;\n if (left.staff === undefined || left.voice !== undefined) continue;\n for (let j = i + 1; j < groups.length; j++) {\n const right = groups[j]!;\n if (right.staff !== undefined || right.voice === undefined) continue;\n if (\n left.position.fraction[0] * right.position.fraction[1] !==\n right.position.fraction[0] * left.position.fraction[1]\n )\n continue;\n if (!scopes.some((scope) => scope.staff === left.staff && scope.voice === right.voice)) continue;\n diagnostics.push({\n code: \"scope-conflict\",\n message: `Dynamic groups ${left.id} and ${right.id} have intersecting equal-specificity scopes at the same time.`,\n groupIds: [left.id, right.id].sort(),\n });\n }\n }\n }\n return diagnostics;\n}\n\nfunction collectRampDiagnostics(lanes: ReadonlyMap<PlaybackLaneId, PlaybackLane>): DynamicPlaybackDiagnostic[] {\n const diagnostics: DynamicPlaybackDiagnostic[] = [];\n const reported = new Set<string>();\n for (const lane of lanes.values()) {\n const ramps = [...lane.envelope.ramps].sort(\n (left, right) => left.startTime - right.startTime || left.groupId.localeCompare(right.groupId),\n );\n for (let index = 1; index < ramps.length; index++) {\n const previous = ramps[index - 1]!;\n const current = ramps[index]!;\n if (current.startTime >= previous.endTime - 1e-6) continue;\n const groupIds = [previous.groupId, current.groupId].sort();\n const key = groupIds.join(\"\\u0000\");\n if (reported.has(key)) continue;\n reported.add(key);\n diagnostics.push({\n code: \"overlapping-gradual\",\n message: `Dynamic groups ${groupIds.join(\" and \")} overlap in playback lane ${lane.id}.`,\n groupIds,\n });\n }\n }\n return diagnostics;\n}\n\n/** Compile standard MNX groups into independently controllable playback lanes. */\nexport function compileDynamicProgram(\n part: Part,\n partIndex: number,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n globalMeasures: readonly GlobalMeasure[],\n gmProgram = -1,\n impliedAnchors: readonly ImpliedSectionDynamicAnchor[] = [],\n staffMeterTable?: StaffMeterTable,\n): DynamicProgram {\n const scoped = part.measures.some((measure) =>\n measure.dynamics?.some((group) => group.staff !== undefined || group.voice !== undefined),\n );\n const scopes = collectLaneScopes(part);\n const selectedScopes = scoped ? scopes : [scopes[0]!];\n const profile = selectDynamicResponseProfile(gmProgram);\n const lanes = new Map<PlaybackLaneId, PlaybackLane>();\n\n for (const scope of selectedScopes) {\n const id = scoped ? playbackLaneId(partIndex, scope.staff, scope.voice) : playbackLaneId(partIndex);\n const lanePart = scoped ? withLaneDynamics(part, scope) : part;\n const envelope = realizeDynamicsEnvelope(\n buildDynamicsEnvelope(\n lanePart,\n measureOrder,\n measureStartBeats,\n model,\n globalMeasures,\n impliedAnchors,\n staffMeterTable,\n ),\n profile,\n );\n lanes.set(id, { id, partIndex, staff: scope.staff, voice: scope.voice, envelope });\n }\n\n return {\n lanes,\n diagnostics: [...collectScopeDiagnostics(part, scopes), ...collectRampDiagnostics(lanes)],\n scoped,\n };\n}\n\n/** Resolve a sequence to its compiled lane. */\nexport function laneForSequence(\n program: DynamicProgram,\n partIndex: number,\n sequence: Sequence,\n sequenceIndex: number,\n): PlaybackLane {\n return laneForScope(program, partIndex, sequence.staff ?? 1, sequenceVoice(sequence, sequenceIndex));\n}\n\n/** Resolve an exact sounding staff/voice scope, including event-level\n * cross-staff overrides within a sequence. */\nexport function laneForScope(program: DynamicProgram, partIndex: number, staff: number, voice: string): PlaybackLane {\n if (!program.scoped) return program.lanes.get(playbackLaneId(partIndex))!;\n const id = playbackLaneId(partIndex, staff, voice);\n return program.lanes.get(id) ?? program.lanes.values().next().value!;\n}\n", "import type { DynamicsEnvelope, DynamicAxes } from \"../dynamicsEnvelope\";\nimport type { DynamicResponseProfile } from \"./types\";\n\nconst STRUCK_PROGRAMS = new Set([46]); // Orchestral harp.\n\n/** Select a runtime response class from a zero-based General MIDI program. */\nexport function selectDynamicResponseProfile(program: number): DynamicResponseProfile {\n if ((program >= 0 && program <= 7) || (program >= 24 && program <= 39) || STRUCK_PROGRAMS.has(program)) {\n return \"struck-plucked\";\n }\n if (program >= 16 && program <= 23) return \"organ-fixed-attack\";\n if ((program >= 40 && program <= 45) || (program >= 47 && program <= 79)) return \"sustained-expressive\";\n return \"fallback\";\n}\n\nfunction realizeAxes(axes: DynamicAxes, profile: DynamicResponseProfile): DynamicAxes {\n if (axes.cc11 === 0) return { velocity: 1, cc11: 0 };\n switch (profile) {\n case \"struck-plucked\":\n return { velocity: axes.velocity, cc11: 118 };\n case \"organ-fixed-attack\":\n return { velocity: 84, cc11: axes.cc11 };\n case \"sustained-expressive\":\n case \"fallback\":\n return axes;\n }\n}\n\n/** Project one semantic envelope through an instrument response profile. */\nexport function realizeDynamicsEnvelope(envelope: DynamicsEnvelope, profile: DynamicResponseProfile): DynamicsEnvelope {\n if (profile === \"fallback\" || profile === \"sustained-expressive\") return envelope;\n return {\n anchors: envelope.anchors.map((anchor) => ({ ...anchor, ...realizeAxes(anchor, profile) })),\n ramps: envelope.ramps.map((ramp) => ({\n ...ramp,\n start: realizeAxes(ramp.start, profile),\n end: realizeAxes(ramp.end, profile),\n })),\n attacks: envelope.attacks.map((attack) => ({\n ...attack,\n attackVelocity: profile === \"organ-fixed-attack\" ? 84 : attack.attackVelocity,\n })),\n };\n}\n", "/** Source part IDs, with deterministic positional fallbacks for missing IDs. */\nexport function stablePartIds(rawMnx: unknown): string[] {\n const raw = typeof rawMnx === \"string\" ? (JSON.parse(rawMnx) as unknown) : rawMnx;\n if (!raw || typeof raw !== \"object\" || !(\"parts\" in raw) || !Array.isArray(raw.parts)) return [];\n return raw.parts.map((part: unknown, index: number) => {\n if (part && typeof part === \"object\" && \"id\" in part && typeof part.id === \"string\" && part.id.length > 0) {\n return part.id;\n }\n return `#${index}`;\n });\n}\n"],
|
|
4
|
+
"sourcesContent": ["{}", "/**\n * @viritura/score-engine \u2014 framework-free music notation rendering kernel.\n *\n * MNX in; layout, ink-only Canvas painting, SVG, geometry queries and a\n * playback timeline out. Page presentation (paper, zoom, scrolling, view\n * modes) belongs to `@viritura/score-viewer`.\n *\n * ```ts\n * import { loadEngine } from \"@viritura/score-engine\";\n *\n * const engine = await loadEngine();\n * const dl = engine.layout(mnx, { pageWidth: 800 });\n * const ctx = canvas.getContext(\"2d\")!;\n * ctx.fillStyle = \"#fff\";\n * ctx.fillRect(0, 0, canvas.width, canvas.height);\n * engine.paint(ctx, dl, { page: 0 });\n * ```\n */\n\nexport { loadEngine, isEngineReady } from \"./engine\";\nexport type { Engine } from \"./engine\";\nexport { EngineLoadError, ParseError, LayoutError } from \"./errors\";\nexport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n DisplayList,\n EngineVersion,\n HorizonPaper,\n LayoutOptions,\n LayoutPageSetup,\n LayoutWorker,\n LayoutWorkerOptions,\n LoadEngineOptions,\n MeasureGeometry,\n PageGeometry,\n PaintOptions,\n PartInfo,\n PlayheadGeometry,\n Rect,\n ScoreInfo,\n ScoreMeasurements,\n ScorePosition,\n SvgOptions,\n SystemGeometry,\n TilePaintOptions,\n TileRenderer,\n} from \"./types\";\nexport type { DynamicMark, TempoSegment, TimedEvent, Timeline, TimelineOptions } from \"./timeline\";\n", "/**\n * `loadEngine()` and the `Engine` handle it resolves to.\n *\n * The engine is the rendering kernel: MNX in, layout + ink + geometry +\n * timeline out. It never touches the DOM beyond the canvas context it is\n * given; page presentation, zoom, scrolling and UI live in the viewer layer.\n */\n\nimport {\n getEngineVersion,\n getScoreInfo,\n getWasmInitError,\n initWasm,\n isWasmReady,\n loadMusicFont,\n resolveBasePath,\n setAssetBasePath,\n wasmComputeMnxScoreLayout,\n} from \"@viritura/renderer\";\nimport { buildCommit, bundledAssetBase, PACKAGE_VERSION } from \"./buildInfo\";\nimport { unwrapDisplayList, wrapDisplayList } from \"./displayListHandle\";\nimport { EngineLoadError, ParseError } from \"./errors\";\nimport {\n canvasToBeat,\n horizonPaper,\n measureGeometry,\n measureLayout,\n playheadAt,\n positionToCanvas,\n systemGeometry,\n} from \"./geometry\";\nimport { layoutArgs, layoutFailure, messageOf, mnxJson } from \"./layoutRequest\";\nimport { createLayoutWorker } from \"./layoutWorkerClient\";\nimport { paintInk } from \"./paint\";\nimport { exportPageSvg } from \"./svgExport\";\nimport { createTileRenderer } from \"./tileRenderer\";\nimport type { Timeline, TimelineOptions } from \"./timeline\";\nimport { buildTimeline } from \"./timelineBuilder\";\nimport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n DisplayList,\n EngineVersion,\n HorizonPaper,\n LayoutOptions,\n LayoutWorker,\n LayoutWorkerOptions,\n LoadEngineOptions,\n MeasureGeometry,\n PaintOptions,\n PlayheadGeometry,\n ScoreInfo,\n ScoreMeasurements,\n ScorePosition,\n SvgOptions,\n SystemGeometry,\n TileRenderer,\n} from \"./types\";\n\n/** The loaded score engine. Acquire via {@link loadEngine}. */\nexport interface Engine {\n readonly version: EngineVersion;\n\n /**\n * Lay out an MNX document. Throws `ParseError` for unreadable input and\n * `LayoutError` if the engine fails.\n */\n layout(mnx: string | object, opts: LayoutOptions): DisplayList;\n\n /**\n * Paint one page's ink into `ctx` under the caller's current transform.\n * Never clears the canvas or paints paper unless `opts.background` is set.\n */\n paint(ctx: CanvasRenderingContext2D, displayList: DisplayList, opts?: PaintOptions): void;\n\n /** Page sizes and parts of a layout. */\n measure(displayList: DisplayList): ScoreMeasurements;\n /** Systems in reading order, page-local. */\n systems(displayList: DisplayList): SystemGeometry[];\n /** Every rendered measure/staff, page-local. */\n measures(displayList: DisplayList): MeasureGeometry[];\n /** Paper rectangle for unpaged (horizon) layouts. */\n horizonPaper(displayList: DisplayList): HorizonPaper;\n\n /** Where a musical position lands on its part's (top) staff. */\n positionToCanvas(displayList: DisplayList, position: ScorePosition): CanvasBeatPosition | null;\n /** A cursor spanning the whole system at a musical position. */\n playhead(displayList: DisplayList, position: ScorePosition): PlayheadGeometry | null;\n /** Hit-test a page-local point to a musical position. */\n canvasToBeat(displayList: DisplayList, page: number, x: number, y: number): CanvasBeatHit | null;\n\n /** Parts, measure count and score entries, without running layout. */\n info(mnx: string | object): ScoreInfo;\n /** Layout-independent playback timeline. Deterministic. */\n timeline(mnx: string | object, opts?: TimelineOptions): Timeline;\n /** Standalone SVG for one page (fonts converted to outlines). */\n toSvg(displayList: DisplayList, opts?: SvgOptions): Promise<string>;\n\n /** Run layout in a dedicated worker. */\n createLayoutWorker(opts?: LayoutWorkerOptions): LayoutWorker;\n /** Cached tile painter for large unpaged layouts. */\n createTileRenderer(): TileRenderer;\n}\n\nlet enginePromise: Promise<Engine> | null = null;\nlet loadedEngine: Engine | null = null;\n\n/**\n * Load the WASM engine and (by default) its fonts. Concurrent calls share one\n * load; a failed load rejects with `EngineLoadError` and may be retried.\n */\nexport function loadEngine(opts: LoadEngineOptions = {}): Promise<Engine> {\n const base = opts.assetBaseUrl ?? bundledAssetBase();\n if (base) setAssetBasePath(base);\n if (enginePromise) return enginePromise;\n const attempt = loadOnce(opts.textFont !== false).then(\n (engine) => {\n loadedEngine = engine;\n return engine;\n },\n (err: unknown) => {\n if (enginePromise === attempt) enginePromise = null;\n throw err;\n },\n );\n enginePromise = attempt;\n return attempt;\n}\n\n/** True once {@link loadEngine} has resolved. */\nexport function isEngineReady(): boolean {\n return loadedEngine !== null;\n}\n\nasync function loadOnce(textFont: boolean): Promise<Engine> {\n await initWasm();\n if (!isWasmReady()) {\n const cause = getWasmInitError();\n throw new EngineLoadError(\n `Failed to initialize WASM engine: ${messageOf(cause ?? \"unknown error\")}`,\n \"wasm\",\n cause,\n );\n }\n let failed: readonly string[];\n try {\n ({ failed } = await loadMusicFont({ textFont }));\n } catch (err) {\n throw new EngineLoadError(`Failed to load fonts: ${messageOf(err)}`, \"font\", err);\n }\n if (failed.includes(\"Bravura\")) {\n throw new EngineLoadError(\"Failed to load the Bravura music font\", \"font\");\n }\n return createEngine();\n}\n\nfunction absoluteAssetBase(): string {\n const base = resolveBasePath();\n if (typeof document === \"undefined\") return base;\n return new URL(base, document.baseURI).href;\n}\n\n/** Build the engine facade. Requires WASM only for layout, info and SVG. */\nexport function createEngine(): Engine {\n const version: EngineVersion = {\n engine: getEngineVersion() ?? \"unknown\",\n package: PACKAGE_VERSION,\n commit: buildCommit(),\n };\n\n return {\n version,\n\n layout(mnx, opts) {\n const args = layoutArgs(mnx, opts);\n try {\n return wrapDisplayList(\n wasmComputeMnxScoreLayout(args.json, args.spatium, args.pageWidth, args.scoreIndex, args.pageSetupJson),\n );\n } catch (err) {\n throw layoutFailure(err);\n }\n },\n\n paint(ctx, displayList, opts = {}) {\n paintInk(ctx, unwrapDisplayList(displayList), opts);\n },\n\n measure: (displayList) => measureLayout(unwrapDisplayList(displayList)),\n systems: (displayList) => systemGeometry(unwrapDisplayList(displayList)),\n measures: (displayList) => measureGeometry(unwrapDisplayList(displayList)),\n horizonPaper: (displayList) => horizonPaper(unwrapDisplayList(displayList)),\n positionToCanvas: (displayList, position) => positionToCanvas(unwrapDisplayList(displayList), position),\n playhead: (displayList, position) => playheadAt(unwrapDisplayList(displayList), position),\n canvasToBeat: (displayList, page, x, y) => canvasToBeat(unwrapDisplayList(displayList), page, x, y),\n\n info(mnx) {\n const json = mnxJson(mnx);\n try {\n const raw = getScoreInfo(json);\n return {\n parts: raw.partIds.map((id, index) => ({ id, index, name: raw.partNames[index] ?? \"\" })),\n measureCount: raw.measureCount,\n scores: raw.scoreNames.map((name, index) => ({ index, name })),\n };\n } catch (err) {\n throw new ParseError(`Failed to read score info: ${messageOf(err)}`, \"schema\", err);\n }\n },\n\n timeline(mnx, opts = {}) {\n try {\n return buildTimeline(mnx, opts);\n } catch (err) {\n if (err instanceof ParseError) throw err;\n throw new ParseError(`Failed to build timeline: ${messageOf(err)}`, \"schema\", err);\n }\n },\n\n toSvg(displayList, opts = {}) {\n return exportPageSvg(unwrapDisplayList(displayList), absoluteAssetBase(), opts);\n },\n\n createLayoutWorker: (opts) => createLayoutWorker(absoluteAssetBase(), opts),\n createTileRenderer,\n };\n}\n", "/**\n * @viritura/renderer \u2014 Canvas 2D score renderer.\n */\n\nexport { ScoreRenderer } from \"./ScoreRenderer\";\nexport { renderScore } from \"./renderScore\";\nexport { paintDisplayList, paintCommand, loadMusicFont } from \"./displayListPainter\";\nexport type { FontLoadResult, LoadMusicFontOptions } from \"./displayListPainter\";\nexport { canvasFontFamily, canvasTextFont, TEXT_FONT_FAMILY } from \"./textFont\";\nexport {\n decodeBinaryDisplayList,\n paintBinaryDisplayList,\n getBinaryDisplayListDimensions,\n getBinaryDisplayListPages,\n getBinaryDisplayListBboxes,\n} from \"./binaryDisplayList\";\nexport { decodeFrame, decodePatchFrame, PatchReconstructor } from \"./patchFrame\";\nexport type { DecodedFrame, PatchFrame, Placement } from \"./patchFrame\";\nexport { GlyphAtlas, COMMON_GLYPHS } from \"./glyphAtlas\";\nexport type { GlyphAtlasConfig } from \"./glyphAtlas\";\nexport { PageCache, splitCommandsByPage } from \"./pageCache\";\nexport {\n SpatialIndex,\n paintSelectionOverlay,\n paintMeasureSelectionOverlay,\n paintHitboxDebug,\n paintHitboxDebugLegend,\n getElementType,\n hitTestSpannerHandle,\n paintSpannerDragPreview,\n} from \"./hitTest\";\nexport { getHitboxDebugKind, HITBOX_DEBUG_STYLES } from \"./hitboxDebug\";\nexport type { HitboxDebugBounds, HitboxDebugKind, HitboxDebugOptions } from \"./hitboxDebug\";\nexport { hitTestBeamInk, isBeamElementId, selectionGroupMembers } from \"./beamHitTest\";\nexport type { ElementBBox, ScoreElementType, SpannerHandleHit, SpannerHandleEnd, DragSnapPoint } from \"./hitTest\";\nexport { paintBeatRuler } from \"./beatRuler\";\nexport type { RulerTick, RulerConfig } from \"./beatRuler\";\nexport {\n initWasm,\n isWasmReady,\n getWasmInitError,\n getEngineVersion,\n computeLayout as wasmComputeLayout,\n computeFullScoreLayout as wasmComputeFullScoreLayout,\n computeLayoutBinary as wasmComputeLayoutBinary,\n computeFullScoreLayoutBinary as wasmComputeFullScoreLayoutBinary,\n computeMnxScoreLayout as wasmComputeMnxScoreLayout,\n computeMnxScoreLayoutBinary as wasmComputeMnxScoreLayoutBinary,\n computeSlurPreview as wasmComputeSlurPreview,\n computeNotePreview as wasmComputeNotePreview,\n getScoreInfo,\n wasmExportSvg,\n createCachedLayoutEngine,\n setEmitLayoutDebug,\n getEmitLayoutDebug,\n EMPTY_LAYOUT_METRICS,\n setWasmTiming,\n} from \"./wasm\";\nexport { createLayoutService } from \"./worker\";\nexport type { LayoutService, AsyncCachedLayoutEngine, LayoutWorkerApi } from \"./worker\";\nexport type {\n DisplayList,\n PartSummary,\n RenderCommand,\n ScoreInfo,\n PageLayout,\n BoundingBox,\n MeasureBounds,\n SelectionGroup,\n SlurGeometry,\n SlurPreview,\n SlurPreviewInput,\n NotePreviewInput,\n WasmSvgPage,\n CachedLayoutEngine,\n LayoutDebugInfo,\n PageTurnWarning,\n SystemDebug,\n AboveBreakdown,\n BelowBreakdown,\n MeasureExtreme,\n MeasureSpacing,\n StaffPairDebug,\n GapInfo,\n PlacementDebug,\n LayoutMetrics,\n} from \"./wasm\";\nexport {\n detectStaves,\n detectHorizonStaves,\n detectStavesForViewMode,\n findStaffAtPosition,\n snapToStaffPosition,\n getStaffPosition,\n paintInputCursor,\n paintGhostNote,\n paintGhostClef,\n extractStickyClefInfo,\n paintStickyClefs,\n paintMeasureNumber,\n} from \"./overlayPainter\";\nexport type { StaffInfo, GhostNoteOptions, GhostClefOptions, StickyClefInfo } from \"./overlayPainter\";\nexport {\n PerfTracker,\n isPerfEnabled,\n enablePerfOverlay,\n disablePerfOverlay,\n isTileCacheDisabled,\n disableTileCache,\n enableTileCache,\n getGlobalPerfTracker,\n} from \"./perfOverlay\";\nexport type { PatchInfo } from \"./perfOverlay\";\nexport { TileCache, DEFAULT_TILE_SIZE, computeDisplayListContentBounds, paintCommandsCulled } from \"./tileCache\";\nexport {\n computeHorizonPaperGeometry,\n paintPaperPage,\n HORIZON_PAPER_PADDING,\n PAPER_SHADOW_MARGIN,\n} from \"./paperPagePainter\";\nexport type { HorizonPaperGeometry } from \"./paperPagePainter\";\nexport { beatToX, paintPlayhead, paintPlayheadAtPosition, findSystemYExtent, findPageForY } from \"./playheadPainter\";\nexport type { PlayheadPosition, PlayheadDraw } from \"./playheadPainter\";\nexport { exportPdf } from \"./pdfPainter\";\nexport type { PdfExportOptions } from \"./pdfPainter\";\nexport { exportSvg, exportSvgPages } from \"./svgPainter\";\nexport type { SvgExportOptions, SvgPage } from \"./svgPainter\";\nexport { resolveBasePath, setAssetBasePath } from \"./basePath\";\n", "import type { NoteValueBase } from \"./enums\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Staff & spacing constants\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Number of staff lines in a standard staff */\nexport const STAFF_LINES = 5;\n\n/** Default spatium (staff space) in pixels at 100% zoom */\nexport const DEFAULT_SPATIUM_PX = 10;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration values (in quarter-note beats)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Map from NoteValueBase to beats (quarter note = 1). Covers the full MNX\n * spec range, from `duplexMaxima` (64 beats) down to `4096th`. */\nexport const DURATION_BEATS: Record<NoteValueBase, number> = {\n duplexMaxima: 64,\n maxima: 32,\n longa: 16,\n breve: 8,\n whole: 4,\n half: 2,\n quarter: 1,\n eighth: 0.5,\n \"16th\": 0.25,\n \"32nd\": 0.125,\n \"64th\": 0.0625,\n \"128th\": 0.03125,\n \"256th\": 0.015625,\n \"512th\": 0.0078125,\n \"1024th\": 0.00390625,\n \"2048th\": 0.001953125,\n \"4096th\": 0.0009765625,\n};\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MIDI pitch mapping\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Semitone offsets for each step (C=0, D=2, E=4, F=5, G=7, A=9, B=11) */\nexport const STEP_SEMITONES: Record<string, number> = {\n C: 0,\n D: 2,\n E: 4,\n F: 5,\n G: 7,\n A: 9,\n B: 11,\n};\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Key signature sharps/flats\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Key signature fifths order for sharps */\nexport const SHARP_ORDER: string[] = [\"F\", \"C\", \"G\", \"D\", \"A\", \"E\", \"B\"];\n\n/** Key signature fifths order for flats */\nexport const FLAT_ORDER: string[] = [\"B\", \"E\", \"A\", \"D\", \"G\", \"C\", \"F\"];\n", "import type { NoteValueBase, StemDirection } from \"../enums\";\nimport type {\n AccidentalEnclosure as RawAccidentalEnclosure,\n AccidentalDisplay as RawAccidentalDisplay,\n Written as RawWritten,\n Note as RawNote,\n Staccato as RawStaccato,\n Staccatissimo as RawStaccatissimo,\n Spiccato as RawSpiccato,\n Tenuto as RawTenuto,\n Accent as RawAccent,\n StrongAccent as RawStrongAccent,\n SoftAccent as RawSoftAccent,\n StressMarking as RawStress,\n UnstressMarking as RawUnstress,\n TremoloSingle as RawTremolo,\n BowDirection as RawBowDirection,\n Fermata as RawFermata,\n Arpeggio as RawArpeggio,\n} from \"../raw\";\nimport type { Pitch } from \"./pitch\";\nimport type { KitNote, NoteheadShape } from \"./kit\";\nimport type { Narrow, HoistVendor } from \"./_derive\";\n\n/**\n * Duration (MNX note value).\n */\nexport interface Duration {\n /** Base note value type */\n base: NoteValueBase;\n /** Number of augmentation dots */\n dots?: number;\n}\n\n/**\n * Tie reference (MNX `tie`). Derived directly from MNX raw \u2014 the decoded\n * shape is identical to the wire shape (target id, targetType enum, side,\n * lv flag, plus global-attrs).\n */\nexport type Tie = import(\"../raw\").Tie;\n\n/**\n * Accidental enclosure symbol (MNX accidental-enclosure-symbol).\n * Derived: MNX raw `accidental-enclosure-symbol` exactly.\n */\nexport type AccidentalEnclosureSymbol = RawAccidentalEnclosure[\"symbol\"];\n\n/**\n * Accidental enclosure (MNX accidental-enclosure). Derived from MNX raw.\n */\nexport type AccidentalEnclosure = RawAccidentalEnclosure;\n\n/**\n * Accidental display control (MNX accidentalDisplay). Derived from MNX raw.\n */\nexport type AccidentalDisplay = RawAccidentalDisplay;\n\n/**\n * Written pitch information for transposed scores (MNX `written`).\n * Derived from MNX raw.\n */\nexport type Written = RawWritten;\n\n/**\n * A single note within an event (a chord can have multiple notes).\n * Derived from MNX raw `note`, with `pitch` narrowed to our `Pitch` type\n * (raw uses an open `octave: number`; we narrow to `Octave = 0..9`).\n *\n * `notehead` is a Viritura vendor extension (`_x.viritura.notehead`) \u2014 MNX has\n * no notehead field on `note` (W3C MNX issue #249). It overrides the default\n * notehead for a single pitched note (e.g. a diamond harmonic inside an\n * otherwise normal chord). Stored per-note because chords mix noteheads.\n */\nexport type Note = HoistVendor<Narrow<RawNote, { pitch: Pitch }>, { notehead?: NoteheadShape }>;\n\n/** Staccato marking. Derived from MNX raw `staccato`. */\nexport type Staccato = RawStaccato;\n\n/** Staccatissimo (wedge staccato) marking. Derived from MNX raw. */\nexport type Staccatissimo = RawStaccatissimo;\n\n/** Staccatissimo wedge variant (Viritura extension; not in MNX spec). */\nexport interface StaccatissimoWedge {\n placement?: Placement;\n}\n\n/** Spiccato marking (staccatissimo stroke, SMuFL U+E4AA). Derived from MNX raw. */\nexport type Spiccato = RawSpiccato;\n\n/** Tenuto marking. Derived from MNX raw. */\nexport type Tenuto = RawTenuto;\n\n/** Accent marking (MNX `accent`). Has only `placement` per MNX v15 spec. */\nexport type Accent = RawAccent;\n\n/** Strong accent (marcato) marking. Derived from MNX raw. */\nexport type StrongAccent = RawStrongAccent;\n\n/** Soft accent (Bart\u00F3k accent) marking \u2014 a hairpin-like wedge (<>). Derived from MNX raw. */\nexport type SoftAccent = RawSoftAccent;\n\n/** Stress marking. Derived from MNX raw `stress-marking`. */\nexport type Stress = RawStress;\n\n/** Unstress marking. Derived from MNX raw `unstress-marking`. */\nexport type Unstress = RawUnstress;\n\n/** Single-note tremolo marking (1\u20133 slashes on the stem). Derived from MNX raw `tremolo-single`. */\nexport type Tremolo = RawTremolo;\n\n/**\n * Bow direction marking (MNX `bowDirection`). Derived from MNX raw.\n * `direction` (required): up = upbow (V), down = downbow (\u2293).\n */\nexport type BowDirection = RawBowDirection;\n\n/** Standard MNX breath-mark symbol type. */\nexport type BreathMarkSymbol = \"comma\" | \"tick\" | \"upbow\" | \"salzedo\" | \"auto\";\n\n/** Breath mark \u2014 indicates a breathing point between notes. */\nexport interface BreathMark {\n /** Symbol style. Omitted and `auto` both leave the choice to the engraver. */\n symbol?: BreathMarkSymbol;\n placement?: Placement;\n}\n\n/** Arpeggio marking \u2014 wavy line to the left of a chord.\n * Derived from MNX raw `arpeggio` (note-level marking). The decoded\n * model only uses `direction`; `arrow`, `position`, `span` live on the\n * measure-level {@link PartMeasureArpeggio} instead. */\nexport type Arpeggio = Pick<RawArpeggio, \"direction\">;\n\n/** Arpeggio direction. */\nexport type ArpeggioDirection = RawArpeggio[\"direction\"];\n\n/** Caesura style variants (MNX `caesura-shape`). */\nexport type CaesuraStyle = \"normal\" | \"thick\" | \"short\" | \"curved\";\n\n/** A caesura (break) marking on an event (native MNX since schema v36). */\nexport interface Caesura {\n /** Style variant (default: \"normal\"). */\n style?: CaesuraStyle;\n /** Number of strokes (default: 2). SMuFL only defines a single-stroke\n * glyph (`caesuraSingleStroke`), so `marks: 1` only renders distinctly\n * for the \"normal\"/\"short\" shapes; \"thick\"/\"curved\" fall back to their\n * standard double-stroke glyph regardless of `marks`. */\n marks?: 1 | 2;\n}\n\n/** Fermata visual symbol (MNX `fermata-symbol`). Derived from MNX raw. */\nexport type FermataSymbol = NonNullable<RawFermata[\"symbol\"]>;\n\n/** Fermata pause duration (MNX `fermata-duration`). Derived from MNX raw. */\nexport type FermataDuration = NonNullable<RawFermata[\"duration\"]>;\n\n/** Symbol placement (MNX `placement` \u2014 above/below/auto). */\nexport type Placement = NonNullable<RawStaccato[\"placement\"]>;\n\n/** Up/down/auto direction (MNX `up-down-auto`). */\nexport type UpDownAuto = NonNullable<RawStrongAccent[\"pointing\"]>;\n\n/** Up/down direction (MNX `up-down`). */\nexport type UpDown = RawBowDirection[\"direction\"];\n\n/** Direction hint for a sequence's stem/notation side (MNX `direction-hint`).\n * Omitting the field means `auto`. The hint resolves stem direction only\n * while another sequence in the same part measure engraves visible content. */\nexport type DirectionHint = import(\"../raw\").DirectionHint;\n\n/** Fermata (hold) marking on a note or rest. Derived from MNX raw. */\nexport type Fermata = RawFermata;\n\n/** Wavy trill-extension line ending at another event. */\nexport interface TrillExtension {\n target: string;\n /** Which rhythmic edge of the target event ends the line. Defaults to \"start\". */\n targetEdge?: \"start\" | \"end\";\n}\n\n/** Trill ornament marking, optionally followed by a wavy extension line. */\nexport interface Trill {\n /** Accidental alteration: -1 = flat, 0 = natural, 1 = sharp */\n accidental?: number;\n /** Whether to display the initial trill symbol. Defaults to true. */\n showSymbol?: boolean;\n /** Wavy extension line from this event to a target event. */\n extension?: TrillExtension;\n}\n\n/** Ornament type variants \u2014 aliased to the Viritura vendor `ornament-type` schema. */\nexport type OrnamentType = import(\"../raw/raw-viritura\").OrnamentType;\n\n/** Fingering annotation \u2014 a digit (0\u20135) placed near a notehead. */\nexport interface Fingering {\n /** Finger number: 0 (thumb in some traditions), 1\u20135. */\n finger: number;\n}\n\n/**\n * Event markings \u2014 articulations and other note-level annotations (MNX markings).\n */\nexport interface Markings {\n staccato?: Staccato;\n staccatissimo?: Staccatissimo;\n staccatissimoWedge?: StaccatissimoWedge;\n spiccato?: Spiccato;\n accent?: Accent;\n tenuto?: Tenuto;\n strongAccent?: StrongAccent;\n softAccent?: SoftAccent;\n stress?: Stress;\n unstress?: Unstress;\n tremolo?: Tremolo;\n breath?: BreathMark;\n /** Bow direction marking (MNX `bowDirection`). */\n bowDirection?: BowDirection;\n trill?: Trill;\n ornaments?: OrnamentType[];\n arpeggio?: Arpeggio;\n /** Caesura (break) marking (native MNX since schema v36). */\n caesura?: Caesura;\n /** Fingering annotations (digits placed near noteheads). */\n fingerings?: Fingering[];\n}\n\n/**\n * MNX \"space\" element \u2014 a rhythmic gap that advances time without rendering.\n */\nexport interface Space {\n type: \"space\";\n /** Duration as a fraction [numerator, denominator] */\n duration: [number, number];\n}\n\n/** Syllable type for a lyric (MNX event-lyric-line-type). */\nexport type LyricLineType = \"start\" | \"middle\" | \"end\" | \"whole\";\n\n/** A single lyric line entry on an event (MNX event-lyric-line). */\nexport interface LyricLine {\n /** The syllable text */\n text: string;\n /** Syllable type indicating word continuation */\n type?: LyricLineType;\n}\n\n/**\n * Lyrics attached to an event (MNX lyrics).\n * The lines map keys are lyric line IDs (e.g., \"1\", \"2\" for verse numbers).\n */\nexport interface Lyrics {\n lines?: Record<string, LyricLine>;\n}\n\n/** Slur line type (MNX lineType). */\nexport type SlurLineType = \"solid\" | \"dashed\" | \"dotted\";\n\n/** Slur side (MNX side / sideEnd). `\"auto\"` and omission both mean automatic placement. */\nexport type SlurSide = \"up\" | \"down\" | \"auto\";\n\n/**\n * Per-slur shape override stored in `_x.viritura.shape`.\n *\n * Each field is a `[dx, dy]` delta in spatia (sp) applied on top of the\n * engine-computed bezier point. Used by engrave-mode handle drags so user\n * edits compose with automatic collision avoidance.\n */\nexport interface SlurShape {\n /** Start endpoint (p0) delta in sp. */\n p0?: [number, number];\n /** First control point (p1) delta in sp. */\n p1?: [number, number];\n /** Second control point (p2) delta in sp. */\n p2?: [number, number];\n /** End endpoint (p3) delta in sp. */\n p3?: [number, number];\n}\n\n/** Slur reference (MNX slur). */\nexport interface Slur {\n target: string;\n side?: SlurSide;\n sideEnd?: SlurSide;\n lineType?: SlurLineType;\n startNote?: string;\n endNote?: string;\n /** Viritura vendor extension: per-handle bezier overrides for engrave mode. */\n shape?: SlurShape;\n}\n\n/** Glissando line style. */\nexport type GlissandoStyle = \"straight\" | \"wavy\";\n\n/** Musical meaning of a glissando-family line, independent of its label. */\nexport type GlissandoKind = \"glissando\" | \"portamento\";\n\n/** Glissando line connecting two notes at different pitches. */\nexport interface Glissando {\n target: string;\n kind?: GlissandoKind;\n style?: GlissandoStyle;\n text?: string;\n showText?: boolean;\n}\n\n/**\n * An event is a single rhythmic moment: a note, chord, or rest.\n * This is the fundamental unit of content in a sequence.\n */\nexport interface NoteEvent {\n type: \"event\";\n /** Unique ID */\n id?: string;\n /** Duration of this event */\n duration: Duration;\n /** Notes in this event (empty/undefined = rest) */\n notes?: Note[];\n /** Drum kit hits in this event (MNX `kitNotes`). Only on percussion parts. */\n kitNotes?: KitNote[];\n /** If present, this is a rest. staffPosition overrides default vertical position. */\n rest?: { staffPosition?: number };\n /** Cross-staff override: render this event on the specified staff number (1-indexed) */\n staff?: number;\n /** Stem direction override */\n stemDirection?: StemDirection;\n /** Slurs starting from this event */\n slurs?: Slur[];\n /** Glissando lines starting from this event */\n glissandos?: Glissando[];\n /** Articulation markings */\n markings?: Markings;\n /** Native MNX fermata (event-level since v15). */\n fermata?: Fermata;\n /** Lyrics attached to this event */\n lyrics?: Lyrics;\n}\n\n/** Grace note type \u2014 how the grace notes affect timing (MNX grace-type). */\nexport type GraceType = \"makeTime\" | \"stealFollowing\" | \"stealPrevious\";\n\n/** A grace note container (MNX grace). */\nexport interface Grace {\n type: \"grace\";\n /** The events within this grace note group */\n content: NoteEvent[];\n /** How the grace notes affect timing */\n graceType?: GraceType;\n /** Whether to display a slash through the flag/beam */\n slash?: boolean;\n /** Optional rendering color (MNX `color`, e.g. \"#ff0000\"). */\n color?: string;\n}\n\n/** Duration specification for a tuplet (inner or outer). */\nexport interface TupletDuration {\n duration: Duration;\n multiple: number;\n}\n\n/** MNX `yes-no-auto` for tuplet bracket display. */\nexport type TupletBracket = \"yes\" | \"no\" | \"auto\";\n\n/** MNX `tuplet-display-setting` for showNumber / showValue. */\nexport type TupletDisplaySetting = \"noNumber\" | \"inner\" | \"both\";\n\n/** Links measure-local tuplet fragments into one cross-barline tuplet. */\nexport interface TupletSpan {\n id: string;\n type: \"start\" | \"continue\" | \"stop\";\n}\n\n/** A tuplet container (MNX tuplet). */\nexport interface Tuplet {\n type: \"tuplet\";\n /** Inner duration: what the content actually spans */\n inner: TupletDuration;\n /** Outer duration: the notated duration the tuplet replaces */\n outer: TupletDuration;\n /** The events (or nested tuplets) within this tuplet */\n content: SequenceContent[];\n /** Whether to show the tuplet bracket (MNX `bracket`). Default: auto. */\n bracket?: TupletBracket;\n /** Which number(s) to display (MNX `showNumber`). Default: inner. */\n showNumber?: TupletDisplaySetting;\n /** Which note value(s) to display (MNX `showValue`). Default: absent. */\n showValue?: TupletDisplaySetting;\n /** Vertical placement (MNX `placement`, above/below/auto) for the tuplet\n * bracket/number. */\n placement?: Placement;\n /** Cross-staff tuplet: render on the specified staff number (1-indexed). */\n staff?: number;\n /** Cross-barline relationship (Viritura extension `_x.viritura.span`). */\n span?: TupletSpan;\n}\n\n/** Multi-note tremolo container (MNX tremolo). */\nexport interface MultiNoteTremolo {\n type: \"tremolo\";\n /** The two events in this tremolo */\n content: NoteEvent[];\n /** Number of tremolo slashes (1, 2, or 3) */\n marks: number;\n /** Outer duration: the total notated duration the tremolo occupies */\n outer: TupletDuration;\n /** Individual note duration override (MNX individual-duration). */\n individualDuration?: Duration;\n}\n\n/**\n * Visual duration for a full-measure rest (MNX fullMeasure).\n */\nexport interface FullMeasure {\n /** The visual duration to display (e.g., whole rest) */\n visualDuration: Duration;\n /** Explicit staff position override (MNX staffPosition). 0 = middle line, positive = up. */\n staffPosition?: number;\n /** Native MNX fermata on this meter-independent bar rest. */\n fermata?: Fermata;\n}\n\n/**\n * A sequence is a single voice within a measure (MNX \"sequence\").\n * Multiple sequences in one measure = multiple voices.\n */\nexport interface Sequence {\n /** Content items (events, tuplets, grace notes, spaces) */\n content: SequenceContent[];\n /** Full-measure rest indicator */\n fullMeasure?: FullMeasure;\n /** Staff number for this sequence (1-indexed; used in grand staff / organ parts) */\n staff?: number;\n /** Voice name for this sequence (MNX voice identifier) */\n voice?: string;\n /** Stem/notation-side hint for this sequence (MNX `directionHint`).\n * Omitting the field means `auto`. */\n directionHint?: DirectionHint;\n}\n\n/**\n * Content within a sequence \u2014 discriminated union of all content types.\n */\nexport type SequenceContent = NoteEvent | Tuplet | Grace | MultiNoteTremolo | Space;\n\n/**\n * Type guard: narrow SequenceContent to NoteEvent.\n */\nexport function isNoteEvent(item: SequenceContent): item is NoteEvent {\n return item.type === \"event\";\n}\n\n/**\n * Helper: is this event a rest?\n */\nexport function isRest(event: NoteEvent): boolean {\n if (event.rest !== undefined) return true;\n const hasNotes = event.notes && event.notes.length > 0;\n const hasKitNotes = event.kitNotes && event.kitNotes.length > 0;\n return !hasNotes && !hasKitNotes;\n}\n", "import type { Grace, MultiNoteTremolo, NoteEvent, SequenceContent, Tuplet } from \"./event\";\n\n/**\n * The content-item kinds that nest child events: `tuplet`, `grace`, and\n * `tremolo`. These are siblings of leaf events in a sequence's flat `content`\n * list \u2014 the only structural difference is that they carry a child `content`\n * array of their own. Centralizing this set is the single source of truth so\n * that adding a future container type is one edit, not a hunt through every\n * tree-walker.\n */\ntype EventContainer = Tuplet | Grace | MultiNoteTremolo;\n\n/** Single source of truth: does this content item nest child events? */\nfunction isEventContainer(item: SequenceContent): item is EventContainer {\n return item.type === \"tuplet\" || item.type === \"grace\" || item.type === \"tremolo\";\n}\n\n/** The child content array of an event container, or null for leaf items. */\nfunction eventContainerContent(item: SequenceContent): readonly SequenceContent[] | null {\n return isEventContainer(item) ? item.content : null;\n}\n\n/**\n * An event paired with the index path that locates it within a sequence's\n * content tree.\n *\n * `path` is the list of indices from the sequence root down to the event:\n * `[i]` for a top-level event at `content[i]`, `[i, j]` for the event at index\n * `j` inside the container at `content[i]`, and so on for nested tuplets. This\n * is the general location representation \u2014 it does not privilege any one\n * container kind, unlike the legacy per-kind index fields.\n */\nexport interface WalkedEvent {\n event: NoteEvent;\n path: number[];\n}\n\n/**\n * Recursively yield every {@link NoteEvent} in a content array together with\n * its index path. This is the canonical event iterator: every place that needs\n * to \"visit all events, descending into containers\" should delegate here rather\n * than re-implementing the descent (and inevitably forgetting a container kind).\n */\nexport function* walkSequenceEvents(\n content: readonly SequenceContent[],\n prefix: readonly number[] = [],\n): Generator<WalkedEvent> {\n for (let i = 0; i < content.length; i++) {\n const item = content[i]!;\n if (item.type === \"event\") {\n yield { event: item, path: [...prefix, i] };\n continue;\n }\n const inner = eventContainerContent(item);\n if (inner) {\n yield* walkSequenceEvents(inner, [...prefix, i]);\n }\n }\n}\n", "import type { Pitch as RawPitch } from \"../raw\";\nimport type { Step, Octave } from \"../enums\";\nimport type { Narrow } from \"./_derive\";\n\n/**\n * A musical pitch. Derived from MNX raw `pitch`, with `octave` narrowed\n * from raw's open `number` to the finite tuple-union `Octave` (0..9) so\n * exhaustive switches and bounds checks work at compile time.\n *\n * Raw fields preserved:\n * - step: \"A\"|\"B\"|\"C\"|\"D\"|\"E\"|\"F\"|\"G\"\n * - alter?: chromatic alteration in semitones\n */\nexport type Pitch = Narrow<RawPitch, { octave: Octave }>;\n\n/**\n * Compute the diatonic staff position of a pitch relative to a reference.\n * Position 0 = reference pitch, positive = above, negative = below.\n * Each step is 1 position (= half a staff space).\n */\nexport function diatonicPosition(pitch: Pitch): number {\n const stepPositions: Record<string, number> = {\n C: 0,\n D: 1,\n E: 2,\n F: 3,\n G: 4,\n A: 5,\n B: 6,\n };\n return stepPositions[pitch.step]! + pitch.octave * 7;\n}\n\n/**\n * Compute MIDI note number for a pitch.\n */\nexport function pitchToMidi(pitch: Pitch): number {\n const stepSemitones: Record<string, number> = {\n C: 0,\n D: 2,\n E: 4,\n F: 5,\n G: 7,\n A: 9,\n B: 11,\n };\n return (pitch.octave + 1) * 12 + stepSemitones[pitch.step]! + (pitch.alter ?? 0);\n}\n\nconst STEPS_ASCENDING: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"];\n\n/**\n * Build a Pitch result, only including alter when defined.\n */\nfunction makePitch(step: Step, octave: Octave, alter: number | undefined): Pitch {\n const result: Pitch = { step, octave };\n if (alter !== undefined) {\n result.alter = alter;\n }\n return result;\n}\n\n/**\n * Move a pitch up by one diatonic step, preserving the alteration.\n * B4 \u2192 C5 (crosses octave boundary).\n */\nexport function stepPitchUp(pitch: Pitch): Pitch {\n const idx = STEPS_ASCENDING.indexOf(pitch.step);\n if (idx === 6) {\n const nextOctave = (pitch.octave + 1) as Octave;\n const clamped = (nextOctave > 9 ? 9 : nextOctave) as Octave;\n return makePitch(\"C\", clamped, pitch.alter);\n }\n return makePitch(STEPS_ASCENDING[idx + 1]!, pitch.octave, pitch.alter);\n}\n\n/**\n * Move a pitch down by one diatonic step, preserving the alteration.\n * C4 \u2192 B3 (crosses octave boundary).\n */\nexport function stepPitchDown(pitch: Pitch): Pitch {\n const idx = STEPS_ASCENDING.indexOf(pitch.step);\n if (idx === 0) {\n const prevOctave = (pitch.octave - 1) as Octave;\n const clamped = (prevOctave < 0 ? 0 : prevOctave) as Octave;\n return makePitch(\"B\", clamped, pitch.alter);\n }\n return makePitch(STEPS_ASCENDING[idx - 1]!, pitch.octave, pitch.alter);\n}\n\nconst STEPS: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"] as const;\n\n/**\n * Construct a Pitch from its diatonic position (C0=0, D0=1, \u2026, B0=6, C1=7, \u2026).\n * Handles negative diatonic positions correctly.\n * @param alter Optional chromatic alteration in semitones.\n */\nexport function pitchFromDiatonic(diatonic: number, alter?: number): Pitch {\n const octave = Math.floor(diatonic / 7) as Octave;\n const stepIndex = ((diatonic % 7) + 7) % 7; // safe modulo for negatives\n const step = STEPS[stepIndex]!;\n return alter !== undefined ? { step, octave, alter } : { step, octave };\n}\n", "import type { Time as RawTime } from \"../raw\";\nimport type { HoistVendor, Narrow } from \"./_derive\";\n\n/**\n * How a meter's beat grouping is presented, independent of the semantic\n * {@link TimeSignatureExtensions.beatStructure} it is derived from.\n *\n * `\"standard\"` engraves an ordinary numeric (or symbolic) meter with no\n * grouping decoration. `\"additive\"` and `\"annotation\"` are only ever\n * engraved for a meter whose resolved beat structure is structurally\n * non-default and has more than one group; see {@link resolveGroupingDisplay}.\n */\nexport type GroupingDisplay = \"standard\" | \"additive\" | \"annotation\";\n\nexport interface TimeSignatureExtensions {\n /** Ordered beat-group lengths in units of the time-signature denominator. */\n beatStructure?: number[];\n /**\n * Explicit per-occurrence grouping-display override for this time\n * signature. Forces the named mode regardless of the document's house\n * style, subject only to the symbolic-display/single-group safety\n * fallback in {@link resolveGroupingDisplay}.\n */\n groupingDisplay?: GroupingDisplay;\n}\n\n/**\n * Time signature. Derived from MNX raw `time`, with the `display` enum\n * extended to cover `\"senzaMisura\"` and `\"note\"` \u2014 Viritura-specific\n * display modes the parser produces from vendor extensions.\n *\n * Field naming preserved: raw `count` (numerator), `unit` (denominator).\n */\nexport type TimeSignature = HoistVendor<\n Narrow<\n RawTime,\n {\n /** Beat unit: 1, 2, 4, 8, 16, 32, 64, 128. Kept as `number` rather than\n * the raw narrow union so editor/UI callers that compute units\n * numerically type-check without per-call widening casts. */\n unit: number;\n display?: \"common\" | \"cut\" | \"senzaMisura\" | \"note\";\n }\n >,\n TimeSignatureExtensions\n>;\n\n/**\n * Compute the total number of quarter-note beats in a measure with this time sig.\n */\nexport function measureBeats(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\nexport interface ResolvedMeter {\n count: number;\n unit: number;\n /** Ordered beat-group lengths in denominator units. */\n beatStructure: number[];\n /** Beat-group boundaries in quarter-note beats, including measure start and end. */\n beatBoundaries: number[];\n source: \"authored\" | \"default\";\n}\n\nconst IRREGULAR_EIGHTH_DEFAULTS: Readonly<Record<number, readonly number[]>> = {\n 5: [3, 2],\n 7: [2, 2, 3],\n 8: [3, 3, 2],\n};\n\nexport function defaultBeatStructure(count: number, unit: number): number[] {\n assertValidTimeSignature(count, unit);\n const irregularShortMeter = unit >= 8 ? IRREGULAR_EIGHTH_DEFAULTS[count] : undefined;\n if (irregularShortMeter) return [...irregularShortMeter];\n if (count % 3 === 0 && (unit >= 8 || count > 3)) {\n return Array.from({ length: count / 3 }, () => 3);\n }\n if (unit >= 8) {\n const quarterUnits = unit / 4;\n const groups: number[] = [];\n for (let remaining = count; remaining > 0; remaining -= quarterUnits) {\n groups.push(Math.min(quarterUnits, remaining));\n }\n return groups;\n }\n return Array.from({ length: count }, () => 1);\n}\n\nexport function resolveMeter(time: TimeSignature): ResolvedMeter {\n assertValidTimeSignature(time.count, time.unit);\n\n const authored = time.beatStructure;\n if (\n authored &&\n (authored.length === 0 ||\n authored.some((group) => !Number.isInteger(group) || group <= 0) ||\n authored.reduce((sum, group) => sum + group, 0) !== time.count)\n ) {\n throw new RangeError(\"Time-signature beatStructure must contain positive integers that sum to count\");\n }\n\n const beatStructure = authored ? [...authored] : defaultBeatStructure(time.count, time.unit);\n const beatBoundaries = [0];\n for (const group of beatStructure) {\n beatBoundaries.push(beatBoundaries[beatBoundaries.length - 1]! + (group * 4) / time.unit);\n }\n return {\n count: time.count,\n unit: time.unit,\n beatStructure,\n beatBoundaries,\n source: authored ? \"authored\" : \"default\",\n };\n}\n\nfunction assertValidTimeSignature(count: number, unit: number): void {\n if (!Number.isInteger(count) || count <= 0) {\n throw new RangeError(\"Time-signature count must be a positive integer\");\n }\n if (![1, 2, 4, 8, 16, 32, 64, 128].includes(unit)) {\n throw new RangeError(\"Time-signature unit must be a supported power-of-two denominator\");\n }\n}\n\n/**\n * Whether a resolved beat structure differs from the meter's conventional\n * default \u2014 i.e. whether it is worth decorating at all. An authored\n * structure that happens to match the automatic default (a \"redundant\"\n * structure) does not count as non-default.\n */\nfunction isNonDefaultGrouping(resolved: ResolvedMeter): boolean {\n const defaults = defaultBeatStructure(resolved.count, resolved.unit);\n return (\n resolved.beatStructure.length !== defaults.length ||\n resolved.beatStructure.some((group, index) => group !== defaults[index])\n );\n}\n\n/**\n * Resolve which {@link GroupingDisplay} mode a rendered meter should\n * actually engrave.\n *\n * Precedence: an explicit staff occurrence override wins outright, then the\n * time signature's own occurrence override (`time.groupingDisplay`), then\n * the document's non-default house style \u2014 but only when `resolved` is\n * structurally non-default (its beat structure differs from the meter's\n * conventional default). Ordinary/default meters always stay `\"standard\"`\n * under the house style, so a plain 4/4 never engraves `\"1+1+1+1\"`.\n *\n * A symbolic display (`common`/`cut`/`senzaMisura`/`note`) or a resolved\n * beat structure of one group has nothing for additive/annotation to show,\n * so both fall back to `\"standard\"` unconditionally \u2014 even under an\n * explicit occurrence or staff override.\n */\nexport function resolveGroupingDisplay(\n time: TimeSignature,\n resolved: ResolvedMeter,\n houseStyle: GroupingDisplay,\n staffOverride?: GroupingDisplay,\n): GroupingDisplay {\n if (time.display || resolved.beatStructure.length <= 1) {\n return \"standard\";\n }\n if (staffOverride) return staffOverride;\n if (time.groupingDisplay) return time.groupingDisplay;\n if (houseStyle !== \"standard\" && isNonDefaultGrouping(resolved)) {\n return houseStyle;\n }\n return \"standard\";\n}\n", "/**\n * Staff-local synchronous meters (`_x.viritura.staffMeters` on a part\n * measure).\n *\n * A staff-local meter lets one staff of a part notate its own time\n * signature while every staff's barlines stay locked to the shared global\n * measure grid \u2014 non-aligning/independent polymeter is explicitly out of\n * scope (see `docs/spec/viritura-extensions.md`).\n *\n * Two synchronization modes reconcile a staff-local meter's duration with\n * the global measure it shares a barline with:\n *\n * - `\"sharedDuration\"`: the staff-local measure duration must equal the\n * global measure duration exactly (e.g. local 6/8 vs. global 3/4 \u2014 both\n * six quarter-note-equivalent beats). Written note durations need no\n * scaling.\n * - `\"fitMeasure\"`: one complete staff-local measure maps onto one complete\n * global measure by a derived exact ratio (e.g. local 6/8 vs. global\n * 2/4 \u2014 two dotted-quarter pulses mapped onto two quarter-note pulses,\n * ratio 2/3).\n *\n * {@link resolveStaffMeterChange} is the single-step algorithm; declarations\n * inherit until changed or reset, so {@link resolveStaffMeterTimeline} walks\n * an ordered run of part measures carrying that inherited state forward.\n * Mirrors the Rust engine's `model::staff_meter` module.\n */\nimport { measureBeats, resolveMeter, type TimeSignature } from \"./time\";\n\n/** A staff-local meter distinct from the global meter. */\nexport interface StaffMeter {\n count: number;\n unit: number;\n /** Ordered beat-group lengths in units of `unit`. */\n beatStructure?: number[];\n}\n\n/**\n * How a staff-local meter's measure duration relates to the global measure\n * it shares a barline with.\n */\nexport type StaffMeterSynchronization = \"sharedDuration\" | \"fitMeasure\";\n\n/** Establishes a staff-local meter from this part measure onward. */\nexport interface StaffMeterSet {\n staff: number;\n meter: StaffMeter;\n synchronization: StaffMeterSynchronization;\n}\n\n/** Resets a staff back to following the global meter. */\nexport interface StaffMeterReset {\n staff: number;\n useGlobal: true;\n}\n\n/**\n * One `_x.viritura.staffMeters[]` entry on a part measure: either sets a\n * staff-local meter and its synchronization mode, or resets the staff back\n * to the global meter. Effective from this measure onward until changed or\n * reset again.\n */\nexport type StaffMeterChange = StaffMeterSet | StaffMeterReset;\n\n/** Narrows a {@link StaffMeterChange} to its reset variant. */\nexport function isStaffMeterReset(change: StaffMeterChange): change is StaffMeterReset {\n return \"useGlobal\" in change;\n}\n\n/** An exact rational ratio, kept as integers to avoid float precision loss. */\nexport interface RationalRatio {\n num: number;\n den: number;\n}\n\nfunction gcd(a: number, b: number): number {\n return b === 0 ? a : gcd(b, a % b);\n}\n\nfunction reduceRatio(num: number, den: number): RationalRatio {\n const divisor = gcd(Math.abs(num), Math.abs(den)) || 1;\n return { num: num / divisor, den: den / divisor };\n}\n\n/** Convert a {@link RationalRatio} to a floating-point multiplier. */\nexport function ratioToNumber(ratio: RationalRatio): number {\n return ratio.num / ratio.den;\n}\n\n/**\n * The resolved, currently-active staff-local meter for one staff at a given\n * point in the score (i.e. after applying the most recent `staffMeters`\n * declaration for that staff, inherited across measures until changed or\n * reset).\n */\nexport interface EffectiveStaffMeter {\n staff: number;\n /** The staff-local meter, expressed as a {@link TimeSignature} so callers\n * can reuse `resolveMeter()`/`ResolvedMeter` directly. */\n timeSignature: TimeSignature;\n synchronization: StaffMeterSynchronization;\n /** Exact ratio of one global measure duration to one staff-local measure\n * duration (`global / local`). `1/1` for `sharedDuration` (the two\n * durations are required to be equal); the derived mapping ratio for\n * `fitMeasure`. */\n ratioToGlobal: RationalRatio;\n}\n\n/** Thrown when a `staffMeters` entry cannot be resolved. */\nexport class StaffMeterValidationError extends RangeError {\n constructor(message: string) {\n super(message);\n this.name = \"StaffMeterValidationError\";\n }\n}\n\nfunction durationRatio(global: TimeSignature, local: TimeSignature): RationalRatio {\n // measure_beats() = count * 4 / unit, so the ratio's unit-4 factors\n // cancel: global/local = (global.count * local.unit) / (global.unit * local.count).\n return reduceRatio(global.count * local.unit, global.unit * local.count);\n}\n\n/**\n * Resolve one `staffMeters` entry (or inherited absence of one) against the\n * previously-effective state and the measure's effective global meter.\n *\n * An inherited meter (`change` is `undefined`) is not returned unchanged \u2014\n * its authored local meter and synchronization mode are preserved, but the\n * ratio (and, for `sharedDuration`, the equal-duration validity check) are\n * re-derived against *this* measure's effective global meter. A global\n * meter change partway through an inherited `fitMeasure` or\n * `sharedDuration` run therefore updates every following measure's\n * `ratioToGlobal`/validity, not just the measure the declaration was\n * authored on.\n *\n * @throws {StaffMeterValidationError} when a `Set` change's meter is\n * malformed, or its (authored or inherited) `sharedDuration`\n * synchronization does not actually share the global measure's duration.\n * Callers that want lenient, best-effort resolution across many measures\n * should use {@link resolveStaffMeterTimeline}, which keeps the previous\n * effective state on error instead of throwing.\n */\nexport function resolveStaffMeterChange(\n previous: EffectiveStaffMeter | undefined,\n change: StaffMeterChange | undefined,\n global: TimeSignature,\n): EffectiveStaffMeter | undefined {\n if (!change) {\n if (!previous) return undefined;\n return resolveLocalMeter(previous.staff, previous.timeSignature, previous.synchronization, global);\n }\n if (isStaffMeterReset(change)) return undefined;\n\n const localTs: TimeSignature = {\n count: change.meter.count,\n unit: change.meter.unit,\n ...(change.meter.beatStructure ? { beatStructure: change.meter.beatStructure } : {}),\n };\n return resolveLocalMeter(change.staff, localTs, change.synchronization, global);\n}\n\n/**\n * Validate a staff-local meter and derive its ratio against `global`. Shared\n * by a fresh `Set` declaration and by re-resolving an inherited meter each\n * measure \u2014 both must apply the exact same rules against whatever global\n * meter is currently in force.\n */\nfunction resolveLocalMeter(\n staff: number,\n localTs: TimeSignature,\n synchronization: StaffMeterSynchronization,\n global: TimeSignature,\n): EffectiveStaffMeter {\n try {\n // Validates count/unit and (if authored) that beatStructure sums to count.\n resolveMeter(localTs);\n } catch (error) {\n throw new StaffMeterValidationError(\n `invalid staff-local meter: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n const localBeats = measureBeats(localTs);\n const globalBeats = measureBeats(global);\n if (synchronization === \"sharedDuration\" && Math.abs(localBeats - globalBeats) > 1e-9) {\n throw new StaffMeterValidationError(\n `sharedDuration requires equal measure durations: staff-local meter is ${localBeats} quarter-note beats, global measure is ${globalBeats}`,\n );\n }\n\n return {\n staff,\n timeSignature: localTs,\n synchronization,\n ratioToGlobal: durationRatio(global, localTs),\n };\n}\n\nfunction refreshRetainedMeter(\n previous: EffectiveStaffMeter | undefined,\n global: TimeSignature,\n): EffectiveStaffMeter | undefined {\n if (!previous) return undefined;\n try {\n return resolveStaffMeterChange(previous, undefined, global);\n } catch {\n return previous;\n }\n}\n\n/** One measure/staff whose `staffMeters` declaration failed to resolve. */\nexport interface StaffMeterIssue {\n measureIndex: number;\n staff: number;\n error: StaffMeterValidationError;\n}\n\n/**\n * Walk an ordered run of a part's measures, applying `staffMeters`\n * declarations/resets in order and carrying inherited state forward.\n *\n * Returns one `Map<staff, EffectiveStaffMeter>` per measure (only staves\n * currently overridden are present; an absent staff follows the global\n * meter), plus every issue encountered. An invalid declaration is skipped \u2014\n * the previously-effective state for that staff is kept \u2014 so a single bad\n * entry cannot corrupt every following measure. Every staff still under an\n * inherited (non-reset) declaration is also re-resolved against each\n * measure's own effective global meter, so a global meter change partway\n * through an inherited run updates `ratioToGlobal` (or surfaces a newly\n * broken `sharedDuration` equality) from that measure onward. Mirrors the\n * Rust engine's `resolve_staff_meter_table`.\n * If a rejected replacement coincides with a global meter change, the\n * retained declaration is re-resolved against that new global meter. When\n * the retained declaration is `sharedDuration` and is no longer valid, its\n * last valid effective value is kept and the authored replacement remains\n * the sole issue for that staff and measure.\n */\nexport function resolveStaffMeterTimeline(\n staffMetersPerMeasure: readonly (StaffMeterChange[] | undefined)[],\n globalTimeAtMeasure: readonly TimeSignature[],\n): { table: Map<number, EffectiveStaffMeter>[]; issues: StaffMeterIssue[] } {\n const state = new Map<number, EffectiveStaffMeter>();\n const table: Map<number, EffectiveStaffMeter>[] = [];\n const issues: StaffMeterIssue[] = [];\n\n for (let measureIndex = 0; measureIndex < staffMetersPerMeasure.length; measureIndex++) {\n const changes = staffMetersPerMeasure[measureIndex];\n const global = globalTimeAtMeasure[measureIndex] ?? { count: 4, unit: 4 };\n const changedStaves = new Set<number>();\n\n if (changes) {\n for (const change of changes) {\n changedStaves.add(change.staff);\n const previous = state.get(change.staff);\n try {\n const effective = resolveStaffMeterChange(previous, change, global);\n if (effective) {\n state.set(change.staff, effective);\n } else {\n state.delete(change.staff);\n }\n } catch (error) {\n issues.push({\n measureIndex,\n staff: change.staff,\n error: error instanceof StaffMeterValidationError ? error : new StaffMeterValidationError(String(error)),\n });\n // The authored replacement was rejected, so the retained\n // declaration is still inherited at this measure. Re-resolve it\n // against the current global meter; if that also fails (possible\n // for sharedDuration), retain its last valid state without\n // reporting a second issue for the same staff and measure.\n const retained = refreshRetainedMeter(previous, global);\n if (retained) state.set(change.staff, retained);\n }\n }\n }\n\n // Re-resolve every other currently-active staff against this measure's\n // effective global meter: an inherited fitMeasure ratio (or\n // sharedDuration validity) depends on the global meter in force at THIS\n // measure, not the one in force when the declaration was authored. On\n // error, keep the previously-effective entry (lenient, matches the\n // fresh-declaration error path above) and surface the issue.\n for (const [staff, previous] of state) {\n if (changedStaves.has(staff)) continue;\n try {\n const effective = resolveStaffMeterChange(previous, undefined, global);\n if (effective) state.set(staff, effective);\n } catch (error) {\n issues.push({\n measureIndex,\n staff,\n error: error instanceof StaffMeterValidationError ? error : new StaffMeterValidationError(String(error)),\n });\n }\n }\n\n table.push(new Map(state));\n }\n\n return { table, issues };\n}\n", "/**\n * ID generation \u2014 UUID v7 for score elements.\n *\n * Format: canonical hyphenated UUID v7 (36 chars, e.g.\n * \"01927f8a-3a4c-7c8d-9e2f-1a2b3c4d5e6f\"). 48-bit unix-millisecond prefix\n * + 74 random bits + version/variant nibbles.\n *\n * Why v7 over v4 or short base36:\n * - Time-ordered: lexicographic sort \u2248 creation order. Useful for log\n * correlation, CRDT op debugging, and (eventually) DB index locality\n * on the server tier (Postgres/SQL Server B-trees love sequential keys).\n * - 74 random bits \u2248 1.9e22 space \u2014 collision-free at any musical scale.\n * - Canonical string parses as `Guid` in C# and `uuid::Uuid` in Rust,\n * so the same id flows through the TS editor, Rust engine, and .NET\n * server with no custom format glue.\n *\n * Browser/Node both ship v4 natively (`crypto.randomUUID()`), so v7 is\n * built by hand below; this is ~20 lines of byte layout, not crypto.\n */\n\nimport type { Score } from \"./model/score\";\n\nconst HEX = \"0123456789abcdef\";\n\nfunction toHex(byte: number): string {\n return HEX[(byte >>> 4) & 0xf]! + HEX[byte & 0xf]!;\n}\n\n/**\n * Generate a UUID v7 (canonical hyphenated form, 36 chars).\n * Uses crypto.getRandomValues when available, falls back to Math.random.\n */\nexport function generateId(): string {\n const bytes = new Uint8Array(16);\n if (typeof globalThis.crypto !== \"undefined\" && globalThis.crypto.getRandomValues) {\n globalThis.crypto.getRandomValues(bytes);\n } else {\n for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n const ts = Date.now();\n // 48-bit big-endian unix-ms timestamp in bytes 0..5\n bytes[0] = Math.floor(ts / 2 ** 40) & 0xff;\n bytes[1] = Math.floor(ts / 2 ** 32) & 0xff;\n bytes[2] = (ts >>> 24) & 0xff;\n bytes[3] = (ts >>> 16) & 0xff;\n bytes[4] = (ts >>> 8) & 0xff;\n bytes[5] = ts & 0xff;\n // Version 7 in high nibble of byte 6\n bytes[6] = (bytes[6]! & 0x0f) | 0x70;\n // RFC 4122 variant (10xx) in high bits of byte 8\n bytes[8] = (bytes[8]! & 0x3f) | 0x80;\n let s = \"\";\n for (let i = 0; i < 16; i++) {\n s += toHex(bytes[i]!);\n if (i === 3 || i === 5 || i === 7 || i === 9) s += \"-\";\n }\n return s;\n}\n\n/**\n * Generate a unique ID that doesn't collide with any ID in the given set.\n */\nexport function generateUniqueId(existingIds: ReadonlySet<string>): string {\n let id = generateId();\n while (existingIds.has(id)) {\n id = generateId();\n }\n return id;\n}\n\n/**\n * Collect all IDs from a score into a Set for collision checking.\n */\nexport function collectScoreIds(score: Score): Set<string> {\n const ids = new Set<string>();\n for (const gm of score.global.measures) {\n if (gm.id) ids.add(gm.id);\n }\n for (const part of score.parts) {\n if (part.id) ids.add(part.id);\n for (const pm of part.measures) {\n for (const dynamic of pm.dynamics ?? []) {\n ids.add(dynamic.id);\n }\n for (const seq of pm.sequences) {\n collectContentIds(seq.content, ids);\n }\n }\n }\n return ids;\n}\n\nfunction collectContentIds(\n content: ReadonlyArray<{\n type: string;\n id?: string;\n notes?: ReadonlyArray<{ id?: string }>;\n content?: ReadonlyArray<unknown>;\n }>,\n ids: Set<string>,\n): void {\n for (const item of content) {\n if (item.id) ids.add(item.id);\n if (item.notes) {\n for (const n of item.notes) {\n if (n.id) ids.add(n.id);\n }\n }\n if (item.content && Array.isArray(item.content)) {\n collectContentIds(\n item.content as ReadonlyArray<{\n type: string;\n id?: string;\n notes?: ReadonlyArray<{ id?: string }>;\n content?: ReadonlyArray<unknown>;\n }>,\n ids,\n );\n }\n }\n}\n", "export {\n appendMeasure,\n insertMeasure,\n deleteMeasure,\n setTimeSignature,\n setKeySignature,\n setRepeatStart,\n setMeasureRepeat,\n setBarline,\n setRepeatEnd,\n setClef,\n setEnding,\n setGroupingDisplayOverride,\n setStaffMeter,\n setStaffMeterToGlobal,\n} from \"./measureOps\";\nexport {\n accentSpelling,\n createDynamicGroup,\n createRelativeDynamicGroup,\n dynamicSpelling,\n isSupportedDynamicGlyph,\n type AuthoredDynamicValue,\n} from \"./dynamicGroups\";\nexport {\n CHORDS_PART_ID,\n UNSUPPORTED_CHORD_MESSAGE,\n parseChordSymbolText,\n formatChordSymbolText,\n rewriteChordSymbolBase,\n resolveChordSymbol,\n transposeChordSymbol,\n voiceChordSymbol,\n compareChordSymbolPositions,\n upsertGlobalChordSymbol,\n mergeGlobalChordSymbols,\n type ChordSymbolResolution,\n type SupportedChordSymbol,\n type ChordSymbolVoicing,\n type ChordSymbolSource,\n type ChordSymbolConflict,\n type ChordSymbolMergeResult,\n} from \"./chordSymbols\";\n", "export { parseChordSymbolText, formatChordSymbolText, rewriteChordSymbolBase } from \"./text\";\nexport {\n CHORDS_PART_ID,\n UNSUPPORTED_CHORD_MESSAGE,\n resolveChordSymbol,\n voiceChordSymbol,\n type ChordSymbolResolution,\n type SupportedChordSymbol,\n type ChordSymbolVoicing,\n} from \"./resolution\";\nexport { transposeChordSymbol } from \"./transpose\";\nexport {\n compareChordSymbolPositions,\n upsertGlobalChordSymbol,\n mergeGlobalChordSymbols,\n type ChordSymbolSource,\n type ChordSymbolConflict,\n type ChordSymbolMergeResult,\n} from \"./global\";\n", "import type { ChordQuality, ChordRoot, ChordSymbol, RhythmicPosition } from \"../../model\";\nimport { resolveChordSymbol } from \"./resolution\";\nimport { formatModifiers, parseModifiers, type ChordModifiers } from \"./modifiers\";\n\nfunction parseRoot(step: string, accidental: string): ChordRoot {\n const normalized = accidental.replaceAll(\"\u266D\", \"b\").replaceAll(\"\u266F\", \"#\");\n const alter =\n normalized === \"x\" || normalized === \"\uD834\uDD2A\"\n ? 2\n : normalized === \"\uD834\uDD2B\"\n ? -2\n : [...normalized].reduce((sum, token) => sum + (token === \"#\" ? 1 : -1), 0);\n return alter === 0 ? { step: step.toUpperCase() } : { step: step.toUpperCase(), alter };\n}\n\n/** Captures whitespace, root, suffix, and optional slash root without rewriting authored syntax. */\nexport const CHORD_TEXT_PATTERN =\n /^(\\s*)([A-Ga-g])((?:#+|b+|\u266F+|\u266D+|x|\uD834\uDD2A|\uD834\uDD2B)?)([^/]*?)(?:\\/([A-Ga-g])((?:#+|b+|\u266F+|\u266D+|x|\uD834\uDD2A|\uD834\uDD2B)?))?(\\s*)$/u;\n\nexport function isNoChordText(text: string): boolean {\n return /^(?:NC|N\\.C\\.?)$/i.test(text.trim());\n}\n\nfunction parseExtension(value: string | undefined): ChordSymbol[\"extension\"] {\n switch (value) {\n case \"6\":\n return 6;\n case \"7\":\n return 7;\n case \"9\":\n return 9;\n case \"11\":\n return 11;\n case \"13\":\n return 13;\n default:\n return undefined;\n }\n}\n\ninterface ParsedQuality {\n quality: ChordQuality;\n extension?: ChordSymbol[\"extension\"];\n modifiers?: ChordModifiers;\n}\n\nfunction parseSpecialQuality(text: string): ParsedQuality | undefined {\n if (text === \"\") return { quality: \"major\" };\n if (text === \"\u0394\" || text === \"\u25B3\") return { quality: \"major\", extension: 7 };\n if (text === \"5\") return { quality: \"power\" };\n if (text.toLowerCase() === \"sus\" || text.toLowerCase() === \"sus4\") return { quality: \"suspended4\" };\n if (text.toLowerCase() === \"sus2\") return { quality: \"suspended2\" };\n if (text.toLowerCase() === \"m7b5\") return { quality: \"half-diminished\", extension: 7 };\n const halfDiminished = /^(?:0|\u00F8)(6|7|9|11|13)?$/.exec(text);\n if (halfDiminished) return { quality: \"half-diminished\", extension: parseExtension(halfDiminished[1]) ?? 7 };\n const minorMajor = /^(?:m|min|-)(?:maj|ma|M|\u0394|\u25B3)(6|7|9|11|13)?$/i.exec(text);\n if (minorMajor) return { quality: \"minor-major\", extension: parseExtension(minorMajor[1]) ?? 7 };\n const suspended = /^(6|7|9|11|13)sus([24])?$/i.exec(text);\n if (suspended)\n return {\n quality: suspended[2] === \"2\" ? \"suspended2\" : \"suspended4\",\n extension: parseExtension(suspended[1]),\n };\n const reverseSuspended = /^sus([24])?(6|7|9|11|13)$/i.exec(text);\n if (reverseSuspended)\n return {\n quality: reverseSuspended[1] === \"2\" ? \"suspended2\" : \"suspended4\",\n extension: parseExtension(reverseSuspended[2]),\n };\n return undefined;\n}\n\nfunction parseBaseQuality(text: string): ParsedQuality | undefined {\n const special = parseSpecialQuality(text);\n if (special) return special;\n const match = /^(maj|Maj|MAJ|ma|Ma|M|m|min|Min|MIN|-|dim|Dim|DIM|o|\u00B0|aug|Aug|AUG|\\+|\u0394|\u25B3)?(6|7|9|11|13)?$/.exec(text);\n if (!match) return undefined;\n\n const marker = match[1] ?? \"\";\n const normalizedMarker = marker.toLowerCase();\n const extension = parseExtension(match[2]);\n\n let quality: ChordQuality;\n switch (normalizedMarker) {\n case \"m\":\n quality = marker === \"M\" ? \"major\" : \"minor\";\n break;\n case \"min\":\n case \"-\":\n quality = \"minor\";\n break;\n case \"dim\":\n case \"o\":\n case \"\u00B0\":\n quality = \"diminished\";\n break;\n case \"aug\":\n case \"+\":\n quality = \"augmented\";\n break;\n case \"maj\":\n case \"ma\":\n case \"\u0394\":\n case \"\u03B4\":\n case \"\u25B3\":\n quality = \"major\";\n break;\n default:\n quality = extension === undefined || extension === 6 ? \"major\" : \"dominant\";\n }\n\n return extension === undefined ? { quality } : { quality, extension };\n}\n\nexport function parseQuality(text: string): ParsedQuality | undefined {\n const boundary = text.search(/add|omit|no|\\(/i);\n if (boundary < 0) return parseBaseQuality(text);\n const base = parseBaseQuality(text.slice(0, boundary).trim());\n const modifiers = parseModifiers(text.slice(boundary));\n if (!base || !modifiers) return undefined;\n if (\n base.quality === \"major\" &&\n base.extension === undefined &&\n modifiers.added.length === 1 &&\n modifiers.added[0] === 6 &&\n modifiers.omitted.length === 0\n ) {\n return { quality: \"major\", extension: 6 };\n }\n return { ...base, modifiers };\n}\n\n/**\n * Parse the common chord-symbol shorthand used by inline entry.\n *\n * The returned event is anchored by its containing measure and relative time.\n * Every input, including malformed/empty text and NC, is preserved verbatim.\n * Unsupported suffixes are never interpreted as major. Use resolveChordSymbol\n * before playback or semantic validation.\n */\nexport function parseChordSymbolText(input: string, position: RhythmicPosition): ChordSymbol {\n const match = CHORD_TEXT_PATTERN.exec(input);\n if (!match) return { position, rawText: input };\n\n const root = parseRoot(match[2]!, match[3] ?? \"\");\n const parsedQuality = parseQuality(match[4] ?? \"\");\n\n const chord: ChordSymbol = {\n position,\n rawText: input,\n root,\n quality: parsedQuality?.quality ?? \"other\",\n };\n if (!parsedQuality || parsedQuality.modifiers) chord.kindText = match[4]!;\n if (parsedQuality?.extension !== undefined) chord.extension = parsedQuality.extension;\n\n if (match[5]) {\n chord.bass = parseRoot(match[5], match[6] ?? \"\");\n }\n\n return chord;\n}\n\nexport function formatChordRoot(root: ChordRoot): string {\n const alter = root.alter ?? 0;\n if (!Number.isSafeInteger(alter) || Math.abs(alter) > 128) return `${root.step}(${alter})`;\n return root.step + (alter > 0 ? \"#\".repeat(alter) : \"b\".repeat(-alter));\n}\n\nfunction formatQuality(chord: ChordSymbol): string {\n const extension = chord.extension;\n switch (chord.quality ?? \"major\") {\n case \"major\":\n return extension === undefined ? \"\" : extension === 6 ? \"6\" : `maj${extension}`;\n case \"minor\":\n return `m${extension ?? \"\"}`;\n case \"dominant\":\n return String(extension ?? 7);\n case \"diminished\":\n return `dim${extension ?? \"\"}`;\n case \"half-diminished\":\n return `\u00F8${extension ?? 7}`;\n case \"augmented\":\n return `aug${extension ?? \"\"}`;\n case \"minor-major\":\n return `mMaj${extension ?? 7}`;\n case \"suspended2\":\n return `${extension ?? \"\"}sus2`;\n case \"suspended4\":\n return `${extension ?? \"\"}sus4`;\n case \"power\":\n return extension === undefined ? \"5\" : `5(${extension})`;\n case \"other\":\n return chord.kindText ?? \"?\";\n }\n}\n\nfunction formatRewriteModifiers(kind: ParsedQuality | undefined, raw: ChordSymbol | undefined): string {\n const rawModifiers = raw?.kindText === undefined ? undefined : parseQuality(raw.kindText)?.modifiers;\n return formatModifiers({\n added: [...new Set([...(kind?.modifiers?.added ?? []), ...(rawModifiers?.added ?? [])])].sort((a, b) => a - b),\n omitted: [...new Set([...(kind?.modifiers?.omitted ?? []), ...(rawModifiers?.omitted ?? [])])].sort(\n (a, b) => a - b,\n ),\n });\n}\n\n/**\n * Rewrite only the base harmony, retaining explicit degree operations and display overrides.\n * Unknown or contradictory source harmony is left untouched rather than guessed.\n */\nexport function rewriteChordSymbolBase(\n chord: ChordSymbol,\n base: Partial<Pick<ChordSymbol, \"quality\" | \"extension\">>,\n): ChordSymbol {\n const raw = chord.rawText === undefined ? undefined : parseChordSymbolText(chord.rawText, chord.position);\n const source = chord.root ? chord : { ...chord, ...raw };\n const kind = source.kindText === undefined ? undefined : parseQuality(source.kindText);\n // Other can carry a recognized suffix after an explicit quality edit; keep that edit reversible.\n const understood =\n source.quality === \"other\" && kind ? { ...source, quality: kind.quality, extension: kind.extension } : source;\n if (resolveChordSymbol({ ...understood, textOverride: undefined }).status !== \"supported\") return chord;\n const next = { ...source, extension: understood.extension, ...base };\n if (next.quality === source.quality && next.extension === source.extension) return chord;\n const modifiers = formatRewriteModifiers(kind, raw);\n const currentKind = formatQuality(understood) + modifiers;\n if (resolveChordSymbol({ ...understood, kindText: currentKind, textOverride: undefined }).status !== \"supported\")\n return chord;\n const nextBase = next.quality === \"other\" ? { ...next, quality: understood.quality } : next;\n next.kindText = modifiers || next.quality === \"other\" ? formatQuality(nextBase) + modifiers : undefined;\n next.rawText = undefined;\n next.rawText = formatChordSymbolText({ ...next, textOverride: undefined });\n if (resolveChordSymbol({ ...next, quality: nextBase.quality, textOverride: undefined }).status !== \"supported\")\n return chord;\n return next;\n}\n\n/** Semantic plain-text label; provenance remains stored separately from explicit display overrides. */\nexport function formatChordSymbolText(chord: ChordSymbol): string {\n if (chord.textOverride !== undefined) return chord.textOverride;\n if (chord.rawText !== undefined) {\n if (resolveChordSymbol(chord).status !== \"supported\") return chord.rawText;\n if (!chord.root) chord = parseChordSymbolText(chord.rawText, chord.position);\n }\n if (!chord.root) return \"\";\n const suffix =\n chord.kindText !== undefined && resolveChordSymbol(chord).status === \"unsupported\"\n ? chord.kindText\n : formatQuality(chord) +\n (chord.kindText ? formatModifiers(parseQuality(chord.kindText)?.modifiers ?? { added: [], omitted: [] }) : \"\");\n return formatChordRoot(chord.root) + suffix + (chord.bass ? `/${formatChordRoot(chord.bass)}` : \"\");\n}\n", "import type { ChordQuality, ChordRoot, ChordSymbol } from \"../../model\";\nimport { isNoChordText, parseChordSymbolText, parseQuality } from \"./text\";\nimport { applyModifiers, type ChordModifiers } from \"./modifiers\";\n\n/** Derived playback channel only: its runtime part index is score.parts.length. Never persist a Part. */\nexport const CHORDS_PART_ID = \"viritura:derived:chords\";\nexport const UNSUPPORTED_CHORD_MESSAGE = \"Unsupported chord: cannot play this symbol.\";\n\nexport interface SupportedChordSymbol {\n status: \"supported\";\n rootPitchClass: number;\n bassPitchClass: number;\n /** Sorted, unique chord tones after degree operations, excluding any non-chord slash bass. */\n pitchClasses: number[];\n}\n\nexport type ChordSymbolResolution =\n SupportedChordSymbol | { status: \"silent\" } | { status: \"unsupported\"; message: typeof UNSUPPORTED_CHORD_MESSAGE };\n\nexport interface ChordSymbolVoicing {\n /** Exactly one root/slash note in MIDI 36..47 for supported symbols. */\n leftHand: number[];\n /** All chord tones in MIDI 60..71, sorted and deduplicated. */\n rightHand: number[];\n}\n\nconst NATURAL_PITCHES: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };\nconst TRIADS: Record<Exclude<ChordQuality, \"other\">, readonly number[]> = {\n major: [0, 4, 7],\n minor: [0, 3, 7],\n dominant: [0, 4, 7],\n diminished: [0, 3, 6],\n \"half-diminished\": [0, 3, 6],\n augmented: [0, 4, 8],\n \"minor-major\": [0, 3, 7],\n suspended2: [0, 2, 7],\n suspended4: [0, 5, 7],\n power: [0, 7],\n};\n\nconst unsupported = (): ChordSymbolResolution => ({ status: \"unsupported\", message: UNSUPPORTED_CHORD_MESSAGE });\nconst modulo = (value: number): number => ((value % 12) + 12) % 12;\n\nfunction pitchClass(root: ChordRoot): number | undefined {\n const natural = NATURAL_PITCHES[root.step];\n const alter = root.alter ?? 0;\n return typeof natural === \"number\" && Number.isSafeInteger(alter) ? modulo(natural + modulo(alter)) : undefined;\n}\n\nfunction chordDegrees(\n quality: Exclude<ChordQuality, \"other\">,\n triad: readonly number[],\n extension: ChordSymbol[\"extension\"],\n): Map<number, number[]> {\n const thirdDegree = quality === \"suspended2\" ? 2 : quality === \"suspended4\" ? 4 : 3;\n const degrees = new Map<number, number[]>([\n [1, [0]],\n [5, [triad[triad.length - 1]!]],\n ]);\n if (quality !== \"power\") degrees.set(thirdDegree, [triad[1]!]);\n // A sixth is additive, not a tertian extension: it does not imply a seventh.\n if (extension === 6) degrees.set(6, [9]);\n else if (extension !== undefined) {\n const seventh = quality === \"major\" || quality === \"minor-major\" ? 11 : quality === \"diminished\" ? 9 : 10;\n degrees.set(7, [seventh]);\n if (extension >= 9) degrees.set(9, [2]);\n if (extension >= 11) degrees.set(11, [5]);\n if (extension >= 13) degrees.set(13, [9]);\n }\n return degrees;\n}\n\nfunction resolveBase(chord: ChordSymbol, modifiers?: ChordModifiers): ChordSymbolResolution {\n if (!chord.root) return unsupported();\n const rootPitchClass = pitchClass(chord.root);\n const bassPitchClass = chord.bass ? pitchClass(chord.bass) : rootPitchClass;\n const quality = chord.quality ?? \"major\";\n if (rootPitchClass === undefined || bassPitchClass === undefined || quality === \"other\") return unsupported();\n const triad = TRIADS[quality];\n if (!Array.isArray(triad)) return unsupported();\n const impliedSeventh = quality === \"dominant\" || quality === \"half-diminished\" || quality === \"minor-major\";\n const extension = chord.extension ?? (impliedSeventh ? 7 : undefined);\n if (extension !== undefined && ![6, 7, 9, 11, 13].includes(extension)) return unsupported();\n if (quality === \"power\" && extension !== undefined) return unsupported();\n const intervals = applyModifiers(chordDegrees(quality, triad, extension), modifiers);\n return {\n status: \"supported\",\n rootPitchClass,\n bassPitchClass,\n pitchClasses: [...new Set(intervals.map((interval) => modulo(rootPitchClass + interval)))].sort((a, b) => a - b),\n };\n}\n\nfunction resolveStructured(chord: ChordSymbol): ChordSymbolResolution {\n const base = resolveBase(chord);\n if (chord.kindText === undefined) return base;\n const parsed = parseQuality(chord.kindText);\n if (!parsed) return unsupported();\n const description = { ...chord, quality: parsed.quality, extension: parsed.extension };\n const described = resolveBase(description);\n if (!equivalent(base, described)) return unsupported();\n const result = resolveBase(chord, parsed.modifiers);\n return equivalent(result, resolveBase(description, parsed.modifiers)) ? result : unsupported();\n}\n\nfunction resolveText(text: string, chord: ChordSymbol): ChordSymbolResolution {\n if (isNoChordText(text)) return { status: \"silent\" };\n return resolveStructured(parseChordSymbolText(text, chord.position));\n}\n\nfunction equivalent(a: ChordSymbolResolution, b: ChordSymbolResolution): boolean {\n if (a.status === \"silent\" && b.status === \"silent\") return true;\n return (\n a.status === \"supported\" &&\n b.status === \"supported\" &&\n a.rootPitchClass === b.rootPitchClass &&\n a.bassPitchClass === b.bassPitchClass &&\n a.pitchClasses.length === b.pitchClasses.length &&\n a.pitchClasses.every((pitch, index) => pitch === b.pitchClasses[index])\n );\n}\n\n/**\n * The single musical interpretation for validation, UI and playback.\n * A missing quality on a structured root means major; unknown qualities never do.\n * Raw syntax and display overrides must agree with structured harmony. NC is\n * silent only when it has no contradictory structured fields or display text.\n */\nexport function resolveChordSymbol(chord: ChordSymbol): ChordSymbolResolution {\n let result: ChordSymbolResolution;\n if (chord.root) {\n result = resolveStructured(chord);\n if (chord.rawText !== undefined && !equivalent(result, resolveText(chord.rawText, chord))) return unsupported();\n } else {\n if (\n chord.rawText === undefined ||\n chord.bass ||\n chord.quality ||\n chord.extension !== undefined ||\n chord.kindText !== undefined\n ) {\n return unsupported();\n }\n result = resolveText(chord.rawText, chord);\n }\n if (chord.textOverride !== undefined && !equivalent(result, resolveText(chord.textOverride, chord)))\n return unsupported();\n return result;\n}\n\n/** Unsupported/NC symbols produce no notes; callers display resolver diagnostics separately. */\nexport function voiceChordSymbol(chord: ChordSymbol): ChordSymbolVoicing {\n const result = resolveChordSymbol(chord);\n if (result.status !== \"supported\") return { leftHand: [], rightHand: [] };\n return {\n leftHand: [36 + result.bassPitchClass],\n rightHand: result.pitchClasses.map((pitch) => 60 + pitch),\n };\n}\n", "export interface ChordModifiers {\n added: number[];\n omitted: number[];\n}\n\n/** Degree lists require an explicit operation; bare numeric extensions are not guessed. */\nexport function parseModifiers(text: string): ChordModifiers | undefined {\n const result: ChordModifiers = { added: [], omitted: [] };\n let rest = text.trim().toLowerCase();\n if (!rest) return result;\n let operation: \"added\" | \"omitted\" | undefined;\n let needsDegree = false;\n let afterDegree = false;\n let inGroup = false;\n let afterComma = false;\n while (rest) {\n const marker = /^(add|omit|no)/.exec(rest)?.[0];\n if (marker) {\n if (needsDegree) return undefined;\n operation = marker === \"add\" ? \"added\" : \"omitted\";\n needsDegree = true;\n afterDegree = false;\n afterComma = false;\n rest = rest.slice(marker.length).trimStart();\n continue;\n }\n const degreeText = /^(13|11|[1-79])/.exec(rest)?.[0];\n if (degreeText) {\n const degree = Number(degreeText);\n if (!operation || (operation === \"added\" && [1, 3, 5].includes(degree))) return undefined;\n result[operation].push(degree);\n needsDegree = false;\n afterDegree = true;\n afterComma = false;\n rest = rest.slice(degreeText.length).trimStart();\n continue;\n }\n if (rest[0] === \"(\" && !inGroup && !afterComma) {\n inGroup = true;\n afterDegree = false;\n } else if (rest[0] === \")\" && inGroup && afterDegree) {\n inGroup = false;\n } else if (rest[0] === \",\" && afterDegree && !afterComma) {\n afterComma = true;\n afterDegree = false;\n } else {\n return undefined;\n }\n rest = rest.slice(1).trimStart();\n }\n if (inGroup || needsDegree || !afterDegree || afterComma) return undefined;\n return {\n added: [...new Set(result.added)].sort((a, b) => a - b),\n omitted: [...new Set(result.omitted)].sort((a, b) => a - b),\n };\n}\n\nexport function formatModifiers(modifiers: ChordModifiers): string {\n return (\n modifiers.added.map((degree) => `add${degree}`).join(\"\") +\n modifiers.omitted.map((degree) => `omit${degree}`).join(\"\")\n );\n}\n\nconst ADDED_INTERVALS: Readonly<Record<number, number>> = { 2: 2, 4: 5, 6: 9, 7: 10, 9: 2, 11: 5, 13: 9 };\n\nexport function applyModifiers(degrees: Map<number, number[]>, modifiers?: ChordModifiers): number[] {\n // Additions name only that degree. In particular add7 is the minor seventh,\n // and add9/11/13 do not imply any lower extensions.\n for (const degree of modifiers?.added ?? []) {\n degrees.set(degree, [...(degrees.get(degree) ?? []), ADDED_INTERVALS[degree]!]);\n }\n for (const degree of modifiers?.omitted ?? []) degrees.delete(degree);\n return [...degrees.values()].flat();\n}\n", "/**\n * Generic conversion / parse diagnostics, shared across @viritura/format and\n * @viritura/musicxml. Surfaces lossy decisions (dropped fields, fallback\n * substitutions, schema violations) so callers (UIs, CLIs, tests) can report\n * them. Not coupled to any particular importer's logging convention \u2014 the\n * shape (severity + RFC 6901 pointer + optional code) is general.\n */\n\nexport type DiagnosticSeverity = \"info\" | \"warning\" | \"error\";\n\n/** A single conversion diagnostic. */\nexport interface MnxDiagnostic {\n /** RFC 6901 JSON Pointer to the offending node, e.g. `/parts/0/measures/2`. */\n pointer: string;\n /** Human-readable message. Should describe what was dropped or substituted. */\n message: string;\n severity: DiagnosticSeverity;\n /** Stable code for filtering in tooling (e.g. `\"unsupported-clef\"`). */\n code?: string;\n}\n\n/**\n * Mutable collector passed through importers / parsers. Use {@link emit}\n * for the common case; instances are stack-local \u2014 there's no global state.\n */\nexport class DiagnosticCollector {\n private readonly entries: MnxDiagnostic[] = [];\n\n emit(d: MnxDiagnostic): void {\n this.entries.push(d);\n }\n\n warn(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"warning\", code });\n }\n\n info(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"info\", code });\n }\n\n error(pointer: string, message: string, code?: string): void {\n this.entries.push({ pointer, message, severity: \"error\", code });\n }\n\n /** Returns a snapshot of the collected diagnostics in emission order. */\n all(): readonly MnxDiagnostic[] {\n return this.entries.slice();\n }\n\n get length(): number {\n return this.entries.length;\n }\n}\n\n/**\n * Build a JSON Pointer by appending segments. Segments are escaped per\n * RFC 6901 (`~` \u2192 `~0`, `/` \u2192 `~1`). Numbers are stringified.\n */\nexport function ptr(...segments: Array<string | number>): string {\n if (segments.length === 0) return \"\";\n return \"/\" + segments.map((s) => String(s).replace(/~/g, \"~0\").replace(/\\//g, \"~1\")).join(\"/\");\n}\n\n/**\n * Compose a child pointer onto an existing parent pointer.\n */\nexport function ptrChild(parent: string, ...segments: Array<string | number>): string {\n return parent + ptr(...segments);\n}\n", "/**\n * DisplayList Canvas Painter \u2014 renders RenderCommands from the WASM engine.\n *\n * This replaces the old painter.ts which did its own layout calculation.\n * Now the layout is computed in Rust/WASM and this just paints the commands.\n */\n\nimport type { DisplayList, RenderCommand } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { resolveBasePath } from \"./basePath\";\nimport { canvasTextFont, TEXT_FONT_FAMILY } from \"./textFont\";\n\n/** Outcome of {@link loadMusicFont}: names of faces that failed to load. */\nexport interface FontLoadResult {\n readonly failed: readonly string[];\n}\n\n/** Options for {@link loadMusicFont}. */\nexport interface LoadMusicFontOptions {\n /**\n * Also load the bundled Libertinus Serif text face as `Viritura Serif`.\n * Default true. When false, text falls back to the page's generic `serif`\n * (or a `Viritura Serif` face the host registers itself).\n */\n readonly textFont?: boolean;\n}\n\ninterface FaceSpec {\n readonly name: string;\n readonly create: (basePath: string) => FontFace;\n}\n\nconst MUSIC_FACES: readonly FaceSpec[] = [\n {\n name: \"Bravura\",\n create: (basePath) =>\n new FontFace(\"Bravura\", `url(${basePath}fonts/Bravura.otf)`, {\n // Restrict to SMuFL Private Use Area + Musical Symbols block.\n // Without this, the browser falls back to Bravura for U+266D (\u266D) etc.\n // in text runs, rendering oversized music glyphs that obscure text labels.\n unicodeRange: \"U+E000-F8FF, U+F0000-FFFFF\",\n }),\n },\n];\n\n// Libertinus Serif \u2014 OFL-licensed text font with \u266D\u266F\u266E support. Registered\n// under a private family (see textFont.ts) so embedding pages keep their\n// own generic `serif`; the engine's DrawText { font: \"serif\" } maps to it.\nconst TEXT_FACES: readonly FaceSpec[] = (\n [\n [\"Libertinus Serif\", \"Regular\", {}],\n [\"Libertinus Serif Bold\", \"Bold\", { weight: \"bold\" }],\n [\"Libertinus Serif Italic\", \"Italic\", { style: \"italic\" }],\n [\"Libertinus Serif Bold Italic\", \"BoldItalic\", { weight: \"bold\", style: \"italic\" }],\n ] as const\n).map(([name, file, descriptors]) => ({\n name,\n create: (basePath: string) =>\n new FontFace(TEXT_FONT_FAMILY, `url(${basePath}fonts/LibertinusSerif-${file}.otf)`, descriptors),\n}));\n\n/**\n * Loads a set of faces once. A result is cached only when the required face\n * loaded, so a failed fetch (network, CSP, wrong asset base) can be retried.\n */\nfunction faceGroupLoader(faces: readonly FaceSpec[], required: string | null): () => Promise<FontLoadResult> {\n let loaded = false;\n let pending: Promise<FontLoadResult> | null = null;\n const load = async (): Promise<FontLoadResult> => {\n const basePath = resolveBasePath();\n const results = await Promise.allSettled(faces.map((face) => face.create(basePath).load()));\n const failed: string[] = [];\n for (const [i, result] of results.entries()) {\n if (result.status === \"fulfilled\") {\n document.fonts.add(result.value);\n } else {\n failed.push(faces[i]!.name);\n console.warn(`Failed to load ${faces[i]!.name} font:`, result.reason);\n }\n }\n return { failed };\n };\n return async () => {\n if (loaded) return { failed: [] };\n if (pending) return pending;\n const attempt = load();\n pending = attempt;\n try {\n const result = await attempt;\n if (required === null || !result.failed.includes(required)) loaded = true;\n return result;\n } catch (e) {\n console.warn(\"Failed to load fonts:\", e);\n return { failed: faces.map((face) => face.name) };\n } finally {\n if (!loaded && pending === attempt) pending = null;\n }\n };\n}\n\nconst loadMusicFaces = faceGroupLoader(MUSIC_FACES, \"Bravura\");\nconst loadTextFaces = faceGroupLoader(TEXT_FACES, null);\n\n/**\n * Load the Bravura SMuFL font (always \u2014 layout metrics are Bravura's) and, by\n * default, the Libertinus Serif text font so Canvas can use them.\n */\nexport async function loadMusicFont(opts: LoadMusicFontOptions = {}): Promise<FontLoadResult> {\n const groups = opts.textFont === false ? [loadMusicFaces()] : [loadMusicFaces(), loadTextFaces()];\n const results = await Promise.all(groups);\n return { failed: results.flatMap((result) => result.failed) };\n}\n\n/**\n * Paint a DisplayList onto a Canvas 2D context.\n * Optionally uses a GlyphAtlas for accelerated glyph rendering.\n */\nexport function paintDisplayList(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas?: GlyphAtlas,\n background: string | null = \"#FFFFFF\",\n): void {\n ctx.clearRect(0, 0, displayList.width, displayList.height);\n if (background !== null) {\n ctx.fillStyle = background;\n ctx.fillRect(0, 0, displayList.width, displayList.height);\n }\n\n // Execute each render command\n for (const cmd of displayList.commands) {\n if (cmd.type === \"DrawGlyph\" && glyphAtlas?.isBuilt && cmd.rotation === 0) {\n const drawn = glyphAtlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintCommand(ctx, cmd);\n }\n}\n\ntype CmdOfType<T extends RenderCommand[\"type\"]> = Extract<RenderCommand, { type: T }>;\n\nfunction paintLine(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawLine\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.lineTo(cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\nfunction paintEllipse(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawEllipse\">): void {\n ctx.save();\n ctx.translate(cmd.cx, cmd.cy);\n ctx.rotate(cmd.angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, cmd.rx, cmd.ry, 0, 0, Math.PI * 2);\n if (cmd.filled) {\n ctx.fillStyle = cmd.color;\n ctx.fill();\n } else {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = Math.max(cmd.rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n}\n\nfunction paintText(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawText\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = canvasTextFont(cmd.font, cmd.size);\n ctx.textAlign = cmd.align;\n ctx.textBaseline = cmd.baseline;\n ctx.fillText(cmd.text, cmd.x, cmd.y);\n}\n\nfunction paintGlyph(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawGlyph\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n // Rust's JSON encoding omits zero rotation (binary commands include it).\n if (cmd.rotation != null && cmd.rotation !== 0) {\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.rotate(cmd.rotation);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(cmd.codepoint), cmd.x, cmd.y);\n }\n}\n\nfunction paintStretchedGlyph(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawStretchedGlyph\">): void {\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.scale(cmd.scale_x, 1);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n}\n\nfunction paintBezier(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawBezier\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.cx1, cmd.cy1, cmd.cx2, cmd.cy2, cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\nfunction paintQuadratic(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawQuadratic\">): void {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.quadraticCurveTo(cmd.cx, cmd.cy, cmd.x2, cmd.y2);\n ctx.stroke();\n}\n\n/**\n * Trace the crescent outline of a solid filled-bezier onto `ctx` as the\n * current path, without painting. Shared by the normal painter and the\n * selection highlight so the two can never disagree about the shape.\n *\n * (x1,y1)/(x2,y2) are outer-contour endpoints; (ix1,iy1)/(ix2,iy2)\n * are inner-contour endpoints. Tips are closed with a small quadratic\n * curve that bulges outward along the local tangent, producing the\n * gentle rounded cap of standard engraving practice.\n */\nexport function traceFilledBezier(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n cmd: CmdOfType<\"DrawFilledBezier\">,\n): void {\n const cap1tx = cmd.x1 - cmd.ocx1;\n const cap1ty = cmd.y1 - cmd.ocy1;\n const cap1tl = Math.hypot(cap1tx, cap1ty) || 1;\n const cap1w = Math.hypot(cmd.x1 - cmd.ix1, cmd.y1 - cmd.iy1);\n const cap1ext = cap1w * 0.55;\n const cap1cx = (cmd.x1 + cmd.ix1) / 2 + (cap1tx / cap1tl) * cap1ext;\n const cap1cy = (cmd.y1 + cmd.iy1) / 2 + (cap1ty / cap1tl) * cap1ext;\n\n const cap2tx = cmd.x2 - cmd.ocx2;\n const cap2ty = cmd.y2 - cmd.ocy2;\n const cap2tl = Math.hypot(cap2tx, cap2ty) || 1;\n const cap2w = Math.hypot(cmd.x2 - cmd.ix2, cmd.y2 - cmd.iy2);\n const cap2ext = cap2w * 0.55;\n const cap2cx = (cmd.x2 + cmd.ix2) / 2 + (cap2tx / cap2tl) * cap2ext;\n const cap2cy = (cmd.y2 + cmd.iy2) / 2 + (cap2ty / cap2tl) * cap2ext;\n\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.ocx1, cmd.ocy1, cmd.ocx2, cmd.ocy2, cmd.x2, cmd.y2);\n ctx.quadraticCurveTo(cap2cx, cap2cy, cmd.ix2, cmd.iy2);\n ctx.bezierCurveTo(cmd.icx2, cmd.icy2, cmd.icx1, cmd.icy1, cmd.ix1, cmd.iy1);\n ctx.quadraticCurveTo(cap1cx, cap1cy, cmd.x1, cmd.y1);\n ctx.closePath();\n}\n\nfunction paintFilledBezierSolid(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawFilledBezier\">): void {\n ctx.fillStyle = cmd.color;\n traceFilledBezier(ctx, cmd);\n ctx.fill();\n}\n\n/**\n * Midline (spine) cubic of a filled bezier \u2014 the average of the outer and\n * inner contours. Dashed slurs/ties are stroked along it.\n */\nexport function filledBezierMidline(cmd: CmdOfType<\"DrawFilledBezier\">): {\n x1: number;\n y1: number;\n cx1: number;\n cy1: number;\n cx2: number;\n cy2: number;\n x2: number;\n y2: number;\n thickness: number;\n} {\n return {\n x1: cmd.x1,\n y1: cmd.y1,\n cx1: (cmd.ocx1 + cmd.icx1) / 2,\n cy1: (cmd.ocy1 + cmd.icy1) / 2,\n cx2: (cmd.ocx2 + cmd.icx2) / 2,\n cy2: (cmd.ocy2 + cmd.icy2) / 2,\n x2: cmd.x2,\n y2: cmd.y2,\n thickness: Math.hypot(cmd.ocx1 - cmd.icx1, cmd.ocy1 - cmd.icy1),\n };\n}\n\nfunction paintFilledBezierDashed(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawFilledBezier\">): void {\n const mid = filledBezierMidline(cmd);\n const strokeWidth = Math.max(mid.thickness * 0.5, 1);\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = strokeWidth;\n if (cmd.line_style === 1) {\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (cmd.line_style === 2) {\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(mid.x1, mid.y1);\n ctx.bezierCurveTo(mid.cx1, mid.cy1, mid.cx2, mid.cy2, mid.x2, mid.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\nfunction paintPolygon(ctx: CanvasRenderingContext2D, cmd: CmdOfType<\"DrawPolygon\">): void {\n const pts = cmd.points;\n if (pts.length < 3) return;\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n const first = pts[0];\n if (first) ctx.moveTo(first[0], first[1]);\n for (let i = 1; i < pts.length; i++) {\n const pt = pts[i];\n if (pt) ctx.lineTo(pt[0], pt[1]);\n }\n ctx.closePath();\n ctx.fill();\n}\n\nexport function paintCommand(ctx: CanvasRenderingContext2D, cmd: RenderCommand): void {\n switch (cmd.type) {\n case \"DrawLine\":\n paintLine(ctx, cmd);\n break;\n case \"DrawEllipse\":\n paintEllipse(ctx, cmd);\n break;\n case \"DrawRect\":\n ctx.fillStyle = cmd.color;\n ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);\n break;\n case \"DrawCircle\":\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n ctx.arc(cmd.cx, cmd.cy, cmd.r, 0, Math.PI * 2);\n ctx.fill();\n break;\n case \"DrawText\":\n paintText(ctx, cmd);\n break;\n case \"DrawGlyph\":\n paintGlyph(ctx, cmd);\n break;\n case \"DrawStretchedGlyph\":\n paintStretchedGlyph(ctx, cmd);\n break;\n case \"DrawBezier\":\n paintBezier(ctx, cmd);\n break;\n case \"DrawQuadratic\":\n paintQuadratic(ctx, cmd);\n break;\n case \"DrawFilledBezier\":\n if (cmd.line_style === 0 || cmd.line_style === undefined) {\n paintFilledBezierSolid(ctx, cmd);\n } else {\n paintFilledBezierDashed(ctx, cmd);\n }\n break;\n case \"DrawPolygon\":\n paintPolygon(ctx, cmd);\n break;\n case \"SetOpacity\":\n ctx.globalAlpha = cmd.opacity;\n break;\n }\n}\n", "let assetBasePathOverride: string | null = null;\n\n/**\n * Override the static asset base used by renderer consumers that cannot rely\n * on a normal web root, such as VS Code webviews.\n */\nexport function setAssetBasePath(basePath: string | null): void {\n if (!basePath) {\n assetBasePathOverride = null;\n return;\n }\n\n assetBasePathOverride = basePath.endsWith(\"/\") ? basePath : `${basePath}/`;\n}\n\n/**\n * Resolve the base URL path for fetching static assets (WASM, fonts, scores).\n *\n * Priority:\n * 1. Explicit override set by embedding hosts such as VS Code webviews\n * 2. HTML <base> tag (set by Storybook / Vite when using a subpath)\n * 3. Vite's import.meta.env.BASE_URL (set at build time)\n * 4. Fallback to \"/\"\n *\n * Always returns a path ending with \"/\".\n */\nexport function resolveBasePath(): string {\n if (assetBasePathOverride) {\n return assetBasePathOverride;\n }\n\n // 1. Check for <base href=\"...\"> tag\n if (typeof document !== \"undefined\") {\n const baseEl = document.querySelector(\"base\");\n if (baseEl) {\n const href = baseEl.getAttribute(\"href\");\n if (href) {\n try {\n const base = new URL(href, window.location.origin).pathname;\n return base.endsWith(\"/\") ? base : base + \"/\";\n } catch {\n // ignore malformed base href\n }\n }\n }\n }\n\n // 2. Vite injects BASE_URL at build time\n try {\n const viteBase = (import.meta as { env?: { BASE_URL?: string } }).env?.BASE_URL;\n if (viteBase) {\n return viteBase.endsWith(\"/\") ? viteBase : viteBase + \"/\";\n }\n } catch {\n // import.meta.env may not exist outside Vite\n }\n\n // 3. Default\n return \"/\";\n}\n", "/**\n * Text-font family resolution for render commands.\n *\n * The engine emits `DrawText { font: \"serif\" }` (optionally followed by style\n * words such as `\"serif italic\"`). Libertinus Serif is registered under a\n * private family name so embedding pages keep their own generic `serif`\n * font; the generic family remains the fallback when the private face is not\n * loaded (for example when a host opts out of font loading).\n */\n\n/** Private FontFace family under which Libertinus Serif is registered. */\nexport const TEXT_FONT_FAMILY = \"Viritura Serif\";\n\nconst SERIF_STACK = `\"${TEXT_FONT_FAMILY}\", serif`;\n\n/** Map a render-command family name to a CSS font-family list. */\nexport function canvasFontFamily(family: string): string {\n return family === \"serif\" ? SERIF_STACK : family;\n}\n\n/**\n * Build a Canvas `font` shorthand from a render-command font name, which may\n * carry style words after the family (`\"serif bold italic\"`).\n */\nexport function canvasTextFont(font: string, size: number): string {\n const parts = font.split(\" \");\n const family = canvasFontFamily(parts[0] || \"serif\");\n const style = parts.slice(1).join(\" \");\n return style ? `${style} ${size}px ${family}` : `${size}px ${family}`;\n}\n", "/**\n * Binary Display List decoder and painter.\n *\n * Reads a packed Float32Array produced by the Rust engine's `to_binary()` method\n * and either decodes it into a DisplayList object or paints directly to a Canvas 2D\n * context (for maximum performance).\n *\n * Binary format documentation: see engine/viritura-engine/src/render/binary.rs\n *\n * Per-tag decode/paint routines live in `./binaryDisplayListCommands` so this\n * file's top-level entry points stay within complexity/length budgets.\n */\n\nimport type {\n DisplayList,\n PageLayout,\n RenderCommand,\n ElementBBox,\n BoundingBox,\n SlurGeometry,\n MeasureBounds,\n SelectionGroup,\n PartSummary,\n} from \"./wasm\";\nimport { BinaryReader, DECODERS, PAINTERS } from \"./binaryDisplayListCommands\";\n\nfunction readPages(r: BinaryReader, numPages: number): PageLayout[] {\n const pages: PageLayout[] = [];\n for (let p = 0; p < numPages; p++) {\n const pageNumber = r.f32();\n const numSystems = r.f32();\n const systemIndices: number[] = [];\n for (let s = 0; s < numSystems; s++) {\n systemIndices.push(r.f32());\n }\n const yOffset = r.f32();\n const height = r.f32();\n pages.push({ pageNumber, systemIndices, yOffset, height });\n }\n return pages;\n}\n\nfunction readElementBboxes(r: BinaryReader, numBboxes: number): ElementBBox[] {\n const bboxes: ElementBBox[] = [];\n for (let i = 0; i < numBboxes; i++) {\n const idLen = r.f32();\n let elementId = \"\";\n for (let j = 0; j < idLen; j++) {\n elementId += String.fromCodePoint(r.f32());\n }\n const bbox: BoundingBox = {\n x: r.f32(),\n y: r.f32(),\n width: r.f32(),\n height: r.f32(),\n };\n bboxes.push({ elementId, bbox });\n }\n return bboxes;\n}\n\nfunction readSlurGeometries(r: BinaryReader, n: number): SlurGeometry[] {\n const out: SlurGeometry[] = [];\n for (let i = 0; i < n; i++) {\n const idLen = r.f32();\n let elementId = \"\";\n for (let j = 0; j < idLen; j++) {\n elementId += String.fromCodePoint(r.f32());\n }\n out.push({\n elementId,\n p0x: r.f32(),\n p0y: r.f32(),\n p1x: r.f32(),\n p1y: r.f32(),\n p2x: r.f32(),\n p2y: r.f32(),\n p3x: r.f32(),\n p3y: r.f32(),\n thickness: r.f32(),\n curveDir: r.f32(),\n sp: r.f32(),\n });\n }\n return out;\n}\n\nfunction readCodepointString(r: BinaryReader, len: number): string {\n let value = \"\";\n for (let i = 0; i < len; i++) {\n value += String.fromCodePoint(r.f32());\n }\n return value;\n}\n\nfunction readMeasureBounds(r: BinaryReader, n: number): MeasureBounds[] {\n const out: MeasureBounds[] = [];\n for (let i = 0; i < n; i++) {\n const idLen = r.f32();\n const measureId = idLen >= 0 ? readCodepointString(r, idLen) : undefined;\n const index = r.f32();\n const partIndex = r.f32();\n const staffIndex = r.f32();\n const systemIndex = r.f32();\n const x = r.f32();\n const width = r.f32();\n const y = r.f32();\n const height = r.f32();\n const prefixWidth = r.f32();\n const totalBeats = r.f32();\n const beatAnchorCount = r.f32();\n const beatAnchors: [number, number][] = [];\n for (let j = 0; j < beatAnchorCount; j++) {\n beatAnchors.push([r.f32(), r.f32()]);\n }\n\n const ghostStaff = r.f32() !== 0;\n const isHidden = r.f32() !== 0;\n const hasMusicHidden = r.f32() !== 0;\n const isExpansion = r.f32() !== 0;\n out.push({\n index,\n ...(measureId !== undefined ? { measureId } : {}),\n partIndex,\n partId: `#${partIndex}`,\n staffIndex,\n systemIndex,\n x,\n width,\n y,\n height,\n prefixWidth,\n totalBeats,\n beatAnchors,\n ...(ghostStaff ? { ghostStaff } : {}),\n ...(isHidden ? { isHidden } : {}),\n ...(hasMusicHidden ? { hasMusicHidden } : {}),\n ...(isExpansion ? { isExpansion } : {}),\n });\n }\n return out;\n}\n\nfunction readSelectionGroups(r: BinaryReader, count: number): SelectionGroup[] {\n const groups: SelectionGroup[] = [];\n for (let index = 0; index < count; index++) {\n const elementId = readCodepointString(r, r.f32());\n const memberCount = r.f32();\n const memberIds: string[] = [];\n for (let member = 0; member < memberCount; member++) {\n memberIds.push(readCodepointString(r, r.f32()));\n }\n groups.push({ elementId, memberIds });\n }\n return groups;\n}\n\nfunction readElementIds(r: BinaryReader, numCommands: number, numStrings: number): (string | null)[] | undefined {\n if (numStrings === 0) {\n // Skip per-command indices (all -1.0)\n r.skip(numCommands);\n return undefined;\n }\n const stringTable: string[] = [];\n for (let s = 0; s < numStrings; s++) {\n const charCount = r.f32();\n let str = \"\";\n for (let i = 0; i < charCount; i++) {\n str += String.fromCodePoint(r.f32());\n }\n stringTable.push(str);\n }\n const ids: (string | null)[] = [];\n for (let c = 0; c < numCommands; c++) {\n const idx = r.f32();\n ids.push(idx < 0 ? null : (stringTable[idx] ?? null));\n }\n return ids;\n}\n\n/**\n * Decode a binary Float32Array into a DisplayList object.\n * Use this when you need the structured data (e.g., for testing or JSON fallback).\n */\nexport function decodeBinaryDisplayList(data: Float32Array): DisplayList {\n const r = new BinaryReader(data);\n\n const width = r.f32();\n const height = r.f32();\n const numCommands = r.f32();\n const numPages = r.f32();\n const numStrings = r.f32();\n const numBboxes = r.f32();\n const numSlurGeometries = r.f32();\n\n const pages = readPages(r, numPages);\n const elementBboxes = readElementBboxes(r, numBboxes);\n const slurGeometries = readSlurGeometries(r, numSlurGeometries);\n\n const commands: RenderCommand[] = [];\n for (let c = 0; c < numCommands; c++) {\n const tag = r.f32();\n const decoder = DECODERS[tag];\n if (!decoder) {\n throw new Error(`Unknown binary display list command tag: ${tag} at offset ${r.pos - 1}`);\n }\n commands.push(decoder(r));\n }\n\n const elementIds = readElementIds(r, numCommands, numStrings);\n const measureBounds = r.pos < data.length ? readMeasureBounds(r, r.f32()) : [];\n const selectionGroups = r.pos < data.length ? readSelectionGroups(r, r.f32()) : [];\n const parts: PartSummary[] = [];\n if (r.pos < data.length) {\n const entryCount = r.f32();\n for (let i = 0; i < entryCount; i++) {\n const boundsIndex = r.f32();\n const sourceCount = r.f32();\n const sources: number[] = [];\n for (let j = 0; j < sourceCount; j++) sources.push(r.f32());\n const bounds = measureBounds[boundsIndex];\n if (bounds && sources.length > 0) bounds.sourcePartIndices = sources;\n }\n }\n if (r.pos < data.length) {\n const count = r.f32();\n for (let i = 0; i < count; i++) {\n parts.push({\n index: r.f32(),\n id: readCodepointString(r, r.f32()),\n name: readCodepointString(r, r.f32()),\n });\n }\n const boundsCount = r.f32();\n for (let i = 0; i < boundsCount; i++) {\n const id = readCodepointString(r, r.f32());\n const bounds = measureBounds[i];\n if (bounds) bounds.partId = id;\n }\n }\n\n const result: DisplayList = { commands, width, height };\n if (pages.length > 0) result.pages = pages;\n if (elementIds) result.elementIds = elementIds;\n if (elementBboxes.length > 0) result.elementBboxes = elementBboxes;\n if (slurGeometries.length > 0) result.slurGeometries = slurGeometries;\n if (measureBounds.length > 0) result.measureBounds = measureBounds;\n if (parts.length > 0) result.parts = parts;\n if (selectionGroups.length > 0) result.selectionGroups = selectionGroups;\n return result;\n}\n\nfunction skipPaintHeaderTables(r: BinaryReader, numPages: number, numBboxes: number, numSlurGeometries: number): void {\n // Skip page data\n for (let p = 0; p < numPages; p++) {\n r.skip(1); // page_number\n const numSystems = r.f32();\n r.skip(numSystems); // system_indices\n r.skip(2); // y_offset, height\n }\n // Skip element bbox data\n for (let i = 0; i < numBboxes; i++) {\n const idLen = r.f32();\n r.skip(idLen); // id codepoints\n r.skip(4); // x, y, width, height\n }\n // Skip slur geometry data\n for (let i = 0; i < numSlurGeometries; i++) {\n const idLen = r.f32();\n r.skip(idLen); // id codepoints\n r.skip(11); // p0..p3 (8) + thickness + curveDir + sp\n }\n}\n\n/**\n * Paint a binary display list directly to a Canvas 2D context.\n * This is the fastest path \u2014 no intermediate object allocation.\n */\nexport function paintBinaryDisplayList(ctx: CanvasRenderingContext2D, data: Float32Array): void {\n const r = new BinaryReader(data);\n\n const width = r.f32();\n const height = r.f32();\n const numCommands = r.f32();\n const numPages = r.f32();\n r.skip(1); // num_strings (not needed for painting)\n const numBboxes = r.f32();\n const numSlurGeometries = r.f32();\n\n skipPaintHeaderTables(r, numPages, numBboxes, numSlurGeometries);\n\n ctx.clearRect(0, 0, width, height);\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, width, height);\n\n for (let c = 0; c < numCommands; c++) {\n const tag = r.f32();\n const painter = PAINTERS[tag];\n if (!painter) {\n throw new Error(`Unknown binary command tag: ${tag}`);\n }\n painter(ctx, r);\n }\n}\n\n/**\n * Extract just the width and height from a binary display list header.\n * Useful for canvas sizing without full decode.\n */\nexport function getBinaryDisplayListDimensions(data: Float32Array): {\n width: number;\n height: number;\n} {\n const r = new BinaryReader(data);\n return { width: r.f32(), height: r.f32() };\n}\n\n/**\n * Extract page layout information from a binary display list.\n */\nexport function getBinaryDisplayListPages(data: Float32Array): PageLayout[] {\n const r = new BinaryReader(data);\n r.skip(3); // width, height, numCommands\n const numPages = r.f32();\n r.skip(3); // num_strings, numBboxes, numSlurGeometries\n return readPages(r, numPages);\n}\n\n/**\n * Extract element bounding boxes from a binary display list.\n */\nexport function getBinaryDisplayListBboxes(data: Float32Array): ElementBBox[] {\n const r = new BinaryReader(data);\n r.skip(3); // width, height, numCommands\n const numPages = r.f32();\n r.skip(1); // numStrings\n const numBboxes = r.f32();\n r.skip(1); // numSlurGeometries\n\n // Skip page data\n for (let p = 0; p < numPages; p++) {\n r.skip(1); // page_number\n const numSystems = r.f32();\n r.skip(numSystems); // system_indices\n r.skip(2); // y_offset, height\n }\n\n return readElementBboxes(r, numBboxes);\n}\n", "/**\n * Per-tag binary display list decoders and painters.\n *\n * Split out of `binaryDisplayList.ts` so that the two top-level entry\n * points (`decodeBinaryDisplayList`, `paintBinaryDisplayList`) stay\n * within complexity and length budgets \u2014 see eslint config.\n */\n\nimport type { RenderCommand } from \"./wasm\";\nimport { canvasFontFamily } from \"./textFont\";\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Tag and table constants (must match Rust binary.rs)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst TAG_DRAW_LINE = 1;\nconst TAG_DRAW_RECT = 2;\nconst TAG_DRAW_CIRCLE = 3;\nconst TAG_DRAW_ELLIPSE = 4;\nconst TAG_DRAW_GLYPH = 5;\nconst TAG_DRAW_BEZIER = 6;\nconst TAG_DRAW_QUADRATIC = 7;\nconst TAG_DRAW_FILLED_BEZIER = 8;\nconst TAG_DRAW_POLYGON = 9;\nconst TAG_DRAW_TEXT = 10;\nconst TAG_SET_OPACITY = 11;\nconst TAG_DRAW_STRETCHED_GLYPH = 12;\n\n// Font IDs match the engine's binary protocol (render/binary.rs):\n// 0 = Bravura (music), then text fonts packed as 1 + family*4 + style where\n// family is serif=0/sans-serif=1/monospace=2 and style is\n// (bold) | (italic << 1).\nconst FONT_TABLE = [\n \"Bravura\",\n \"serif\",\n \"serif\",\n \"serif\",\n \"serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"sans-serif\",\n \"monospace\",\n \"monospace\",\n \"monospace\",\n \"monospace\",\n] as const;\nconst FONT_STYLE_TABLE = [\n \"\",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n \"\",\n \"bold \",\n \"italic \",\n \"bold italic \",\n] as const;\nconst FONT_DECODE_TABLE = [\n \"Bravura\",\n \"serif\",\n \"serif bold\",\n \"serif italic\",\n \"serif bold italic\",\n \"sans-serif\",\n \"sans-serif bold\",\n \"sans-serif italic\",\n \"sans-serif bold italic\",\n \"monospace\",\n \"monospace bold\",\n \"monospace italic\",\n \"monospace bold italic\",\n] as const;\n\nconst ALIGN_TABLE: readonly CanvasTextAlign[] = [\"left\", \"center\", \"right\"];\nconst BASELINE_TABLE: readonly CanvasTextBaseline[] = [\"top\", \"middle\", \"bottom\", \"alphabetic\"];\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// BinaryReader \u2014 fast typed-array reader for the binary buffer.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport class BinaryReader {\n private readonly f32Data: Float32Array;\n private readonly u32Data: Uint32Array;\n pos: number = 0;\n\n constructor(data: Float32Array) {\n this.f32Data = data;\n this.u32Data = new Uint32Array(data.buffer, data.byteOffset, data.length);\n }\n\n f32(): number {\n const val = this.f32Data[this.pos] ?? 0;\n this.pos++;\n return val;\n }\n\n u32(): number {\n const val = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return val;\n }\n\n color(): string {\n const rgb = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return \"#\" + (rgb & 0xffffff).toString(16).padStart(6, \"0\").toUpperCase();\n }\n\n colorFast(): string {\n const rgb = this.u32Data[this.pos] ?? 0;\n this.pos++;\n return \"#\" + (rgb & 0xffffff).toString(16).padStart(6, \"0\");\n }\n\n skip(n: number): void {\n this.pos += n;\n }\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Per-tag DECODERS (structured RenderCommand emit)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype Decoder = (r: BinaryReader) => RenderCommand;\n\nfunction decodeDrawLine(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawLine\",\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawRect(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawRect\",\n x: r.f32(),\n y: r.f32(),\n w: r.f32(),\n h: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawCircle(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawCircle\",\n cx: r.f32(),\n cy: r.f32(),\n r: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawEllipse(r: BinaryReader): RenderCommand {\n const cx = r.f32(),\n cy = r.f32();\n const rx = r.f32(),\n ry = r.f32();\n const angle = r.f32();\n const filled = r.f32() !== 0;\n const color = r.color();\n return { type: \"DrawEllipse\", cx, cy, rx, ry, angle, filled, color };\n}\n\nfunction decodeDrawGlyph(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n const color = r.color();\n const rotation = r.f32();\n return {\n type: \"DrawGlyph\",\n x,\n y,\n codepoint,\n font: FONT_TABLE[fontId] ?? \"serif\",\n size,\n color,\n rotation,\n };\n}\n\nfunction decodeDrawStretchedGlyph(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n const color = r.color();\n const scale_x = r.f32();\n return {\n type: \"DrawStretchedGlyph\",\n x,\n y,\n codepoint,\n font: FONT_TABLE[fontId] ?? \"serif\",\n size,\n color,\n scale_x,\n };\n}\n\nfunction decodeDrawBezier(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawBezier\",\n x1: r.f32(),\n y1: r.f32(),\n cx1: r.f32(),\n cy1: r.f32(),\n cx2: r.f32(),\n cy2: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawQuadratic(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawQuadratic\",\n x1: r.f32(),\n y1: r.f32(),\n cx: r.f32(),\n cy: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n width: r.f32(),\n color: r.color(),\n };\n}\n\nfunction decodeDrawFilledBezier(r: BinaryReader): RenderCommand {\n return {\n type: \"DrawFilledBezier\",\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n ocx1: r.f32(),\n ocy1: r.f32(),\n ocx2: r.f32(),\n ocy2: r.f32(),\n icx1: r.f32(),\n icy1: r.f32(),\n icx2: r.f32(),\n icy2: r.f32(),\n ix1: r.f32(),\n iy1: r.f32(),\n ix2: r.f32(),\n iy2: r.f32(),\n color: r.color(),\n line_style: r.f32(),\n };\n}\n\nfunction decodeDrawPolygon(r: BinaryReader): RenderCommand {\n const nPoints = r.f32();\n const points: [number, number][] = [];\n for (let i = 0; i < nPoints; i++) {\n points.push([r.f32(), r.f32()]);\n }\n return { type: \"DrawPolygon\", points, color: r.color() };\n}\n\nfunction decodeDrawText(r: BinaryReader): RenderCommand {\n const x = r.f32(),\n y = r.f32();\n const size = r.f32();\n const color = r.color();\n const alignIdx = r.f32();\n const baselineIdx = r.f32();\n const fontId = r.f32();\n const textLen = r.f32();\n let text = \"\";\n for (let i = 0; i < textLen; i++) {\n text += String.fromCodePoint(r.f32());\n }\n return {\n type: \"DrawText\",\n x,\n y,\n size,\n color,\n align: (ALIGN_TABLE[alignIdx] ?? \"left\") as \"left\" | \"center\" | \"right\",\n baseline: (BASELINE_TABLE[baselineIdx] ?? \"alphabetic\") as \"top\" | \"middle\" | \"bottom\" | \"alphabetic\",\n font: FONT_DECODE_TABLE[fontId] ?? \"serif\",\n text,\n };\n}\n\nfunction decodeSetOpacity(r: BinaryReader): RenderCommand {\n return { type: \"SetOpacity\", opacity: r.f32() };\n}\n\nexport const DECODERS: Record<number, Decoder> = {\n [TAG_DRAW_LINE]: decodeDrawLine,\n [TAG_DRAW_RECT]: decodeDrawRect,\n [TAG_DRAW_CIRCLE]: decodeDrawCircle,\n [TAG_DRAW_ELLIPSE]: decodeDrawEllipse,\n [TAG_DRAW_GLYPH]: decodeDrawGlyph,\n [TAG_DRAW_STRETCHED_GLYPH]: decodeDrawStretchedGlyph,\n [TAG_DRAW_BEZIER]: decodeDrawBezier,\n [TAG_DRAW_QUADRATIC]: decodeDrawQuadratic,\n [TAG_DRAW_FILLED_BEZIER]: decodeDrawFilledBezier,\n [TAG_DRAW_POLYGON]: decodeDrawPolygon,\n [TAG_DRAW_TEXT]: decodeDrawText,\n [TAG_SET_OPACITY]: decodeSetOpacity,\n};\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Per-tag PAINTERS (direct canvas paint)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype Painter = (ctx: CanvasRenderingContext2D, r: BinaryReader) => void;\n\nfunction paintDrawLine(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32(),\n x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.lineTo(x2, y2);\n ctx.stroke();\n}\n\nfunction paintDrawRect(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32(),\n w = r.f32(),\n h = r.f32();\n ctx.fillStyle = r.colorFast();\n ctx.fillRect(x, y, w, h);\n}\n\nfunction paintDrawCircle(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const cx = r.f32(),\n cy = r.f32(),\n radius = r.f32();\n ctx.fillStyle = r.colorFast();\n ctx.beginPath();\n ctx.arc(cx, cy, radius, 0, Math.PI * 2);\n ctx.fill();\n}\n\nfunction paintDrawEllipse(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const cx = r.f32(),\n cy = r.f32(),\n rx = r.f32(),\n ry = r.f32();\n const angle = r.f32();\n const filled = r.f32() !== 0;\n const color = r.colorFast();\n ctx.save();\n ctx.translate(cx, cy);\n ctx.rotate(angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, rx, ry, 0, 0, Math.PI * 2);\n if (filled) {\n ctx.fillStyle = color;\n ctx.fill();\n } else {\n ctx.strokeStyle = color;\n ctx.lineWidth = Math.max(rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n}\n\nfunction paintDrawGlyph(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const rotation = r.f32();\n ctx.font = `${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n if (rotation !== 0) {\n ctx.save();\n ctx.translate(x, y);\n ctx.rotate(rotation);\n ctx.fillText(String.fromCodePoint(codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(codepoint), x, y);\n }\n}\n\nfunction paintDrawStretchedGlyph(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const codepoint = r.f32();\n const fontId = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const scaleX = r.f32();\n ctx.font = `${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(x, y);\n ctx.scale(scaleX, 1);\n ctx.fillText(String.fromCodePoint(codepoint), 0, 0);\n ctx.restore();\n}\n\nfunction paintDrawBezier(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32();\n const cx1 = r.f32(),\n cy1 = r.f32();\n const cx2 = r.f32(),\n cy2 = r.f32();\n const x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.bezierCurveTo(cx1, cy1, cx2, cy2, x2, y2);\n ctx.stroke();\n}\n\nfunction paintDrawQuadratic(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x1 = r.f32(),\n y1 = r.f32();\n const cx = r.f32(),\n cy = r.f32();\n const x2 = r.f32(),\n y2 = r.f32();\n const lineWidth = r.f32();\n ctx.strokeStyle = r.colorFast();\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(x1, y1);\n ctx.quadraticCurveTo(cx, cy, x2, y2);\n ctx.stroke();\n}\n\ninterface FilledBezierGeom {\n x1: number;\n y1: number;\n x2: number;\n y2: number;\n ocx1: number;\n ocy1: number;\n ocx2: number;\n ocy2: number;\n icx1: number;\n icy1: number;\n icx2: number;\n icy2: number;\n ix1: number;\n iy1: number;\n ix2: number;\n iy2: number;\n}\n\nfunction paintFilledBezierSolid(ctx: CanvasRenderingContext2D, g: FilledBezierGeom, fillColor: string): void {\n // Solid: filled crescent shape with finite-width rounded tips.\n // Cap is a quadratic curve bulging outward along the local tangent.\n const cap1tx = g.x1 - g.ocx1;\n const cap1ty = g.y1 - g.ocy1;\n const cap1tl = Math.hypot(cap1tx, cap1ty) || 1;\n const cap1w = Math.hypot(g.x1 - g.ix1, g.y1 - g.iy1);\n const cap1ext = cap1w * 0.55;\n const cap1cx = (g.x1 + g.ix1) / 2 + (cap1tx / cap1tl) * cap1ext;\n const cap1cy = (g.y1 + g.iy1) / 2 + (cap1ty / cap1tl) * cap1ext;\n\n const cap2tx = g.x2 - g.ocx2;\n const cap2ty = g.y2 - g.ocy2;\n const cap2tl = Math.hypot(cap2tx, cap2ty) || 1;\n const cap2w = Math.hypot(g.x2 - g.ix2, g.y2 - g.iy2);\n const cap2ext = cap2w * 0.55;\n const cap2cx = (g.x2 + g.ix2) / 2 + (cap2tx / cap2tl) * cap2ext;\n const cap2cy = (g.y2 + g.iy2) / 2 + (cap2ty / cap2tl) * cap2ext;\n\n ctx.fillStyle = fillColor;\n ctx.beginPath();\n ctx.moveTo(g.x1, g.y1);\n ctx.bezierCurveTo(g.ocx1, g.ocy1, g.ocx2, g.ocy2, g.x2, g.y2);\n ctx.quadraticCurveTo(cap2cx, cap2cy, g.ix2, g.iy2);\n ctx.bezierCurveTo(g.icx2, g.icy2, g.icx1, g.icy1, g.ix1, g.iy1);\n ctx.quadraticCurveTo(cap1cx, cap1cy, g.x1, g.y1);\n ctx.closePath();\n ctx.fill();\n}\n\nfunction paintFilledBezierStroked(\n ctx: CanvasRenderingContext2D,\n g: FilledBezierGeom,\n fillColor: string,\n lineStyle: number,\n): void {\n // Dashed/dotted: stroke through the midline of outer+inner curves\n // standard engraving practice renders dashed slurs as dashed bezier strokes.\n const mcx1 = (g.ocx1 + g.icx1) / 2;\n const mcy1 = (g.ocy1 + g.icy1) / 2;\n const mcx2 = (g.ocx2 + g.icx2) / 2;\n const mcy2 = (g.ocy2 + g.icy2) / 2;\n const thickness = Math.hypot(g.ocx1 - g.icx1, g.ocy1 - g.icy1);\n const strokeWidth = Math.max(thickness * 0.5, 1);\n ctx.strokeStyle = fillColor;\n ctx.lineWidth = strokeWidth;\n if (lineStyle === 1) {\n // Dashed\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (lineStyle === 2) {\n // Dotted\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n // Unknown style \u2014 fallback to solid stroke\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(g.x1, g.y1);\n ctx.bezierCurveTo(mcx1, mcy1, mcx2, mcy2, g.x2, g.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\nfunction paintDrawFilledBezier(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const g: FilledBezierGeom = {\n x1: r.f32(),\n y1: r.f32(),\n x2: r.f32(),\n y2: r.f32(),\n ocx1: r.f32(),\n ocy1: r.f32(),\n ocx2: r.f32(),\n ocy2: r.f32(),\n icx1: r.f32(),\n icy1: r.f32(),\n icx2: r.f32(),\n icy2: r.f32(),\n ix1: r.f32(),\n iy1: r.f32(),\n ix2: r.f32(),\n iy2: r.f32(),\n };\n const fillColor = r.colorFast();\n const lineStyle = r.f32();\n if (lineStyle === 0) {\n paintFilledBezierSolid(ctx, g, fillColor);\n } else {\n paintFilledBezierStroked(ctx, g, fillColor, lineStyle);\n }\n}\n\nfunction paintDrawPolygon(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const nPoints = r.f32();\n if (nPoints >= 3) {\n ctx.beginPath();\n ctx.moveTo(r.f32(), r.f32());\n for (let i = 1; i < nPoints; i++) {\n ctx.lineTo(r.f32(), r.f32());\n }\n ctx.fillStyle = r.colorFast();\n ctx.closePath();\n ctx.fill();\n } else {\n r.skip(nPoints * 2 + 1);\n }\n}\n\nfunction paintDrawText(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n const x = r.f32(),\n y = r.f32();\n const size = r.f32();\n ctx.fillStyle = r.colorFast();\n const alignIdx = r.f32();\n const baselineIdx = r.f32();\n const fontId = r.f32();\n const textLen = r.f32();\n let text = \"\";\n for (let i = 0; i < textLen; i++) {\n text += String.fromCodePoint(r.f32());\n }\n ctx.font = `${FONT_STYLE_TABLE[fontId] ?? \"\"}${size}px ${canvasFontFamily(FONT_TABLE[fontId] ?? \"serif\")}`;\n ctx.textAlign = ALIGN_TABLE[alignIdx] ?? \"left\";\n ctx.textBaseline = BASELINE_TABLE[baselineIdx] ?? \"alphabetic\";\n ctx.fillText(text, x, y);\n}\n\nfunction paintSetOpacity(ctx: CanvasRenderingContext2D, r: BinaryReader): void {\n ctx.globalAlpha = r.f32();\n}\n\nexport const PAINTERS: Record<number, Painter> = {\n [TAG_DRAW_LINE]: paintDrawLine,\n [TAG_DRAW_RECT]: paintDrawRect,\n [TAG_DRAW_CIRCLE]: paintDrawCircle,\n [TAG_DRAW_ELLIPSE]: paintDrawEllipse,\n [TAG_DRAW_GLYPH]: paintDrawGlyph,\n [TAG_DRAW_STRETCHED_GLYPH]: paintDrawStretchedGlyph,\n [TAG_DRAW_BEZIER]: paintDrawBezier,\n [TAG_DRAW_QUADRATIC]: paintDrawQuadratic,\n [TAG_DRAW_FILLED_BEZIER]: paintDrawFilledBezier,\n [TAG_DRAW_POLYGON]: paintDrawPolygon,\n [TAG_DRAW_TEXT]: paintDrawText,\n [TAG_SET_OPACITY]: paintSetOpacity,\n};\n", "import type { RenderCommand } from \"./wasm\";\n\nexport interface RenderBounds {\n x: number;\n y: number;\n x2: number;\n y2: number;\n}\n\n/** Conservative command bounds shared by direct-paint and retained-layer culling. */\nexport function renderCommandBounds(cmd: RenderCommand): RenderBounds | null {\n switch (cmd.type) {\n case \"DrawEllipse\":\n return { x: cmd.cx - cmd.rx, y: cmd.cy - cmd.ry, x2: cmd.cx + cmd.rx, y2: cmd.cy + cmd.ry };\n case \"DrawLine\":\n return {\n x: Math.min(cmd.x1, cmd.x2) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2) + cmd.width,\n };\n case \"DrawRect\":\n return { x: cmd.x, y: cmd.y, x2: cmd.x + cmd.w, y2: cmd.y + cmd.h };\n case \"DrawCircle\":\n return { x: cmd.cx - cmd.r, y: cmd.cy - cmd.r, x2: cmd.cx + cmd.r, y2: cmd.cy + cmd.r };\n case \"DrawGlyph\":\n return { x: cmd.x - cmd.size, y: cmd.y - cmd.size, x2: cmd.x + cmd.size * 2, y2: cmd.y + cmd.size };\n case \"DrawStretchedGlyph\":\n // A stretched glyph is only ever narrowed, so the proportional box still\n // contains it; its height is the one dimension `size` speaks for.\n return { x: cmd.x - cmd.size, y: cmd.y - cmd.size, x2: cmd.x + cmd.size * 2, y2: cmd.y + cmd.size };\n case \"DrawText\": {\n const charCount = cmd.text?.length ?? 1;\n const estimatedWidth = charCount * cmd.size * 0.6;\n const left =\n cmd.align === \"right\" ? cmd.x - estimatedWidth : cmd.align === \"center\" ? cmd.x - estimatedWidth / 2 : cmd.x;\n return { x: left, y: cmd.y - cmd.size, x2: left + estimatedWidth, y2: cmd.y + cmd.size * 0.3 };\n }\n case \"DrawBezier\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.cx1, cmd.cx2) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2, cmd.cx1, cmd.cx2) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2) + cmd.width,\n };\n case \"DrawQuadratic\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.cx) - cmd.width,\n y: Math.min(cmd.y1, cmd.y2, cmd.cy) - cmd.width,\n x2: Math.max(cmd.x1, cmd.x2, cmd.cx) + cmd.width,\n y2: Math.max(cmd.y1, cmd.y2, cmd.cy) + cmd.width,\n };\n case \"DrawFilledBezier\":\n return {\n x: Math.min(cmd.x1, cmd.x2, cmd.ocx1, cmd.ocx2, cmd.icx1, cmd.icx2),\n y: Math.min(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n x2: Math.max(cmd.x1, cmd.x2, cmd.ocx1, cmd.ocx2, cmd.icx1, cmd.icx2),\n y2: Math.max(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n };\n case \"DrawPolygon\": {\n if (cmd.points.length === 0) return null;\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const [px, py] of cmd.points) {\n minX = Math.min(minX, px);\n minY = Math.min(minY, py);\n maxX = Math.max(maxX, px);\n maxY = Math.max(maxY, py);\n }\n return { x: minX, y: minY, x2: maxX, y2: maxY };\n }\n case \"SetOpacity\":\n return null;\n }\n}\n", "/**\n * PageCache \u2014 OffscreenCanvas page cache with 5-page sliding window.\n *\n * Each page is rendered once to its own OffscreenCanvas. The visible canvas\n * composites cached pages via drawImage() for 60fps scrolling.\n *\n * Ref: docs/spec/performance-architecture.md \u00A73.1\n */\n\nimport type { DisplayList, PageLayout, RenderCommand } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { traceFilledBezier } from \"./displayListPainter\";\nimport { canvasTextFont } from \"./textFont\";\n\n/** Sliding window size: current page \u00B1 2. */\nconst WINDOW_SIZE = 5;\nconst WINDOW_BEFORE = 2;\n\ninterface CachedPage {\n pageIndex: number;\n canvas: OffscreenCanvas;\n width: number;\n height: number;\n}\n\n/**\n * Split a DisplayList into per-page command arrays using the pages metadata.\n * Commands are assigned to a page based on their y-coordinate falling within\n * the page's yOffset..yOffset+height range.\n */\nexport function splitCommandsByPage(displayList: DisplayList): { commands: RenderCommand[]; layout: PageLayout }[] {\n const pages = displayList.pages;\n if (!pages || pages.length === 0) {\n // No page info \u2014 treat entire displayList as one page\n return [\n {\n commands: displayList.commands,\n layout: {\n pageNumber: 1,\n systemIndices: [],\n yOffset: 0,\n height: displayList.height,\n },\n },\n ];\n }\n\n const result: { commands: RenderCommand[]; layout: PageLayout }[] = pages.map((p) => ({ commands: [], layout: p }));\n\n for (const cmd of displayList.commands) {\n const [cmdMinY, cmdMaxY] = getCommandYRange(cmd);\n // Assign to every page whose vertical extent overlaps the command's\n // y-range. This is what makes cross-page geometry (slurs that bridge a\n // page break, brackets that span systems, long beam lines, etc.) render\n // correctly: each page's OffscreenCanvas paints its slice, and the\n // out-of-bounds remainder is silently clipped by Canvas 2D.\n let assigned = false;\n for (let i = 0; i < pages.length; i++) {\n const page = pages[i];\n if (!page) continue;\n const pageTop = page.yOffset;\n const pageBottom = page.yOffset + page.height;\n if (cmdMaxY >= pageTop && cmdMinY < pageBottom) {\n const entry = result[i];\n if (entry) entry.commands.push(cmd);\n assigned = true;\n }\n }\n // If no page matched (e.g. global background), assign to first visible page\n if (!assigned && pages.length > 0) {\n const first = result[0];\n if (first) first.commands.push(cmd);\n }\n }\n\n return result;\n}\n\n/**\n * Extract the [minY, maxY] vertical extent of a render command. Used to\n * decide which page(s) a command belongs to so cross-page geometry (slurs,\n * brackets, long beams) renders on every page it crosses.\n */\nfunction getCommandYRange(cmd: RenderCommand): [number, number] {\n switch (cmd.type) {\n case \"DrawLine\":\n return [Math.min(cmd.y1, cmd.y2), Math.max(cmd.y1, cmd.y2)];\n case \"DrawRect\":\n return [cmd.y, cmd.y + cmd.h];\n case \"DrawCircle\":\n return [cmd.cy - cmd.r, cmd.cy + cmd.r];\n case \"DrawEllipse\":\n return [cmd.cy - cmd.ry, cmd.cy + cmd.ry];\n case \"DrawText\":\n return [cmd.y, cmd.y];\n case \"DrawGlyph\":\n return [cmd.y, cmd.y];\n case \"DrawStretchedGlyph\":\n // A brace hangs from its baseline, so the span it covers is the `size`\n // above `y` \u2014 a page it crosses has to keep it.\n return [cmd.y - cmd.size, cmd.y];\n case \"DrawBezier\":\n return [Math.min(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2), Math.max(cmd.y1, cmd.y2, cmd.cy1, cmd.cy2)];\n case \"DrawQuadratic\":\n return [Math.min(cmd.y1, cmd.y2, cmd.cy), Math.max(cmd.y1, cmd.y2, cmd.cy)];\n case \"DrawFilledBezier\":\n return [\n Math.min(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n Math.max(cmd.y1, cmd.y2, cmd.ocy1, cmd.ocy2, cmd.icy1, cmd.icy2),\n ];\n case \"DrawPolygon\": {\n if (cmd.points.length === 0) return [0, 0];\n const ys = cmd.points.map((p) => p[1]);\n return [Math.min(...ys), Math.max(...ys)];\n }\n case \"SetOpacity\":\n return [0, 0];\n }\n}\n\nexport class PageCache {\n private cache: Map<number, CachedPage> = new Map();\n private totalPages = 0;\n private displayList: DisplayList | null = null;\n private pageCommands: { commands: RenderCommand[]; layout: PageLayout }[] = [];\n private glyphAtlas: GlyphAtlas | null = null;\n\n /** Optional painter function override (for testing). */\n private paintFn:\n ((ctx: OffscreenCanvasRenderingContext2D, commands: RenderCommand[], atlas: GlyphAtlas | null) => void) | null =\n null;\n\n constructor(glyphAtlas?: GlyphAtlas) {\n if (glyphAtlas) {\n this.glyphAtlas = glyphAtlas;\n }\n }\n\n /** Set the glyph atlas for optimized rendering. */\n setGlyphAtlas(atlas: GlyphAtlas): void {\n this.glyphAtlas = atlas;\n }\n\n /** Set a custom paint function (for testing). */\n setPaintFn(\n fn: (ctx: OffscreenCanvasRenderingContext2D, commands: RenderCommand[], atlas: GlyphAtlas | null) => void,\n ): void {\n this.paintFn = fn;\n }\n\n /**\n * Load a display list and split it by pages.\n * Invalidates the entire cache.\n */\n setDisplayList(displayList: DisplayList): void {\n this.displayList = displayList;\n this.pageCommands = splitCommandsByPage(displayList);\n this.totalPages = this.pageCommands.length;\n this.cache.clear();\n }\n\n /** Number of pages in the current display list. */\n get pageCount(): number {\n return this.totalPages;\n }\n\n /**\n * Ensure the 5-page window around currentPage is cached.\n * Evicts pages outside the window.\n */\n ensureWindow(currentPage: number): void {\n if (!this.displayList) return;\n\n const windowStart = Math.max(0, currentPage - WINDOW_BEFORE);\n const windowEnd = Math.min(this.totalPages - 1, windowStart + WINDOW_SIZE - 1);\n\n // Evict pages outside window\n for (const [pageIndex] of this.cache) {\n if (pageIndex < windowStart || pageIndex > windowEnd) {\n this.cache.delete(pageIndex);\n }\n }\n\n // Render pages in the window\n for (let i = windowStart; i <= windowEnd; i++) {\n if (!this.cache.has(i)) {\n this.renderPage(i);\n }\n }\n }\n\n /**\n * Render a single page to its own OffscreenCanvas.\n */\n private renderPage(pageIndex: number): void {\n if (pageIndex < 0 || pageIndex >= this.totalPages || !this.displayList) return;\n\n const pageData = this.pageCommands[pageIndex];\n if (!pageData) return;\n const width = this.displayList.width;\n const height = pageData.layout.height;\n\n const canvas = new OffscreenCanvas(Math.ceil(width), Math.ceil(height));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n // White background\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, width, height);\n\n // Translate so page content renders at y=0\n const yOff = pageData.layout.yOffset;\n ctx.translate(0, -yOff);\n\n if (this.paintFn) {\n this.paintFn(ctx, pageData.commands, this.glyphAtlas);\n } else {\n // Use the imported paint function\n paintCommandsWithAtlas(ctx, pageData.commands, this.glyphAtlas);\n }\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n\n this.cache.set(pageIndex, { pageIndex, canvas, width, height });\n }\n\n /**\n * Composite cached pages onto the visible canvas.\n */\n compositeToCanvas(\n ctx: CanvasRenderingContext2D,\n canvasWidth: number,\n canvasHeight: number,\n scrollY: number = 0,\n ): void {\n ctx.clearRect(0, 0, canvasWidth, canvasHeight);\n ctx.fillStyle = \"#FFFFFF\";\n ctx.fillRect(0, 0, canvasWidth, canvasHeight);\n\n for (const [, page] of this.cache) {\n const pageData = this.pageCommands[page.pageIndex];\n if (!pageData) continue;\n\n const pageTop = pageData.layout.yOffset - scrollY;\n const pageBottom = pageTop + page.height;\n\n // Skip if not visible\n if (pageBottom < 0 || pageTop > canvasHeight) continue;\n\n ctx.drawImage(page.canvas, 0, pageTop);\n }\n }\n\n /** Check if a specific page is cached. */\n isPageCached(pageIndex: number): boolean {\n return this.cache.has(pageIndex);\n }\n\n /** Get the number of currently cached pages. */\n get cachedPageCount(): number {\n return this.cache.size;\n }\n\n /** Invalidate the entire cache (e.g. on zoom change). */\n invalidate(): void {\n this.cache.clear();\n }\n}\n\n/**\n * Paint a list of commands using the glyph atlas when possible,\n * falling back to direct rendering.\n */\nfunction paintCommandsWithAtlas(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n commands: RenderCommand[],\n atlas: GlyphAtlas | null,\n): void {\n for (const cmd of commands) {\n if (cmd.type === \"DrawGlyph\" && atlas?.isBuilt && cmd.rotation === 0) {\n const drawn = atlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintSingleCommand(ctx, cmd);\n }\n}\n\ntype Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;\ntype FilledBezierCmd = Extract<RenderCommand, { type: \"DrawFilledBezier\" }>;\n\nfunction paintFilledBezierCached(ctx: Ctx2D, cmd: FilledBezierCmd): void {\n if (cmd.line_style === 0 || cmd.line_style === undefined) {\n ctx.fillStyle = cmd.color;\n traceFilledBezier(ctx, cmd);\n ctx.fill();\n return;\n }\n const mcx1 = (cmd.ocx1 + cmd.icx1) / 2;\n const mcy1 = (cmd.ocy1 + cmd.icy1) / 2;\n const mcx2 = (cmd.ocx2 + cmd.icx2) / 2;\n const mcy2 = (cmd.ocy2 + cmd.icy2) / 2;\n const thickness = Math.hypot(cmd.ocx1 - cmd.icx1, cmd.ocy1 - cmd.icy1);\n const strokeWidth = Math.max(thickness * 0.5, 1);\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = strokeWidth;\n if (cmd.line_style === 1) {\n ctx.lineCap = \"butt\";\n ctx.setLineDash([Math.max(strokeWidth * 2.4, 5), Math.max(strokeWidth * 1.8, 4)]);\n } else if (cmd.line_style === 2) {\n ctx.lineCap = \"round\";\n ctx.setLineDash([0.001, Math.max(strokeWidth * 2.2, 3.5)]);\n } else {\n ctx.lineCap = \"round\";\n ctx.setLineDash([]);\n }\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(mcx1, mcy1, mcx2, mcy2, cmd.x2, cmd.y2);\n ctx.stroke();\n ctx.setLineDash([]);\n}\n\n/**\n * Paint a single render command (fallback path, same as displayListPainter).\n */\nfunction paintSingleCommand(\n ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,\n cmd: RenderCommand,\n): void {\n switch (cmd.type) {\n case \"DrawLine\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.lineTo(cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawEllipse\":\n ctx.save();\n ctx.translate(cmd.cx, cmd.cy);\n ctx.rotate(cmd.angle);\n ctx.beginPath();\n ctx.ellipse(0, 0, cmd.rx, cmd.ry, 0, 0, Math.PI * 2);\n if (cmd.filled) {\n ctx.fillStyle = cmd.color;\n ctx.fill();\n } else {\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = Math.max(cmd.rx * 0.05, 1);\n ctx.stroke();\n }\n ctx.restore();\n break;\n\n case \"DrawRect\":\n ctx.fillStyle = cmd.color;\n ctx.fillRect(cmd.x, cmd.y, cmd.w, cmd.h);\n break;\n\n case \"DrawCircle\":\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n ctx.arc(cmd.cx, cmd.cy, cmd.r, 0, Math.PI * 2);\n ctx.fill();\n break;\n\n case \"DrawText\":\n ctx.fillStyle = cmd.color;\n ctx.font = canvasTextFont(cmd.font, cmd.size);\n ctx.textAlign = cmd.align;\n ctx.textBaseline = cmd.baseline;\n ctx.fillText(cmd.text, cmd.x, cmd.y);\n break;\n\n case \"DrawGlyph\":\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n if (cmd.rotation !== 0) {\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.rotate(cmd.rotation);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n } else {\n ctx.fillText(String.fromCodePoint(cmd.codepoint), cmd.x, cmd.y);\n }\n break;\n\n case \"DrawStretchedGlyph\":\n ctx.fillStyle = cmd.color;\n ctx.font = `${cmd.size}px ${cmd.font}`;\n ctx.textAlign = \"left\";\n ctx.textBaseline = \"alphabetic\";\n ctx.save();\n ctx.translate(cmd.x, cmd.y);\n ctx.scale(cmd.scale_x, 1);\n ctx.fillText(String.fromCodePoint(cmd.codepoint), 0, 0);\n ctx.restore();\n break;\n\n case \"DrawBezier\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.bezierCurveTo(cmd.cx1, cmd.cy1, cmd.cx2, cmd.cy2, cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawQuadratic\":\n ctx.strokeStyle = cmd.color;\n ctx.lineWidth = cmd.width;\n ctx.lineCap = \"round\";\n ctx.beginPath();\n ctx.moveTo(cmd.x1, cmd.y1);\n ctx.quadraticCurveTo(cmd.cx, cmd.cy, cmd.x2, cmd.y2);\n ctx.stroke();\n break;\n\n case \"DrawFilledBezier\":\n paintFilledBezierCached(ctx, cmd);\n break;\n\n case \"DrawPolygon\": {\n const pts = cmd.points;\n if (pts.length >= 3) {\n ctx.fillStyle = cmd.color;\n ctx.beginPath();\n const first = pts[0];\n if (first) ctx.moveTo(first[0], first[1]);\n for (let i = 1; i < pts.length; i++) {\n const pt = pts[i];\n if (pt) ctx.lineTo(pt[0], pt[1]);\n }\n ctx.closePath();\n ctx.fill();\n }\n break;\n }\n }\n}\n", "/**\n * WASM Engine wrapper \u2014 loads and initializes the Viritura WASM module.\n *\n * This wraps the wasm-bindgen generated JS glue code and provides\n * a clean TypeScript API for the editor to use.\n */\n\nimport { decodeBinaryDisplayList } from \"./binaryDisplayList\";\n\n// Type re-exports \u2014 definitions live in wasmTypes.ts.\nexport * from \"./wasmTypes\";\nimport type {\n BoundingBox,\n DisplayList,\n NotePreviewInput,\n RenderCommand,\n ScoreInfo,\n SlurPreview,\n SlurPreviewInput,\n} from \"./wasmTypes\";\n\ndeclare const __VIRITURA_WASM_ASSET_HASH__: string | undefined;\n\n// Module-level state\nlet wasmModule: {\n compute_layout: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_layout_binary: (\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_full_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => string;\n compute_full_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ) => Float32Array;\n compute_mnx_score_layout: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => string;\n compute_mnx_score_layout_binary: (\n mnx_json: string,\n spatium: number,\n page_width: number,\n score_index: number,\n page_setup_json?: string,\n ) => Float32Array;\n get_score_info: (mnx_json: string) => string;\n engine_version: () => string;\n compute_slur_preview: (preview_json: string) => string;\n compute_note_preview?: (preview_json: string) => string;\n export_svg: (\n display_list_json: string,\n bravura_data: Uint8Array,\n text_font_data: Uint8Array,\n spatium_mm: number,\n sp_pixels: number,\n page_width_mm: number,\n page_height_mm: number,\n ) => string;\n set_emit_layout_debug?: (enabled: boolean) => void;\n get_emit_layout_debug?: () => boolean;\n set_wasm_timing?: (enabled: boolean) => void;\n} | null = null;\n\nlet initPromise: Promise<void> | null = null;\nlet lastInitError: unknown = null;\n\n/** The error from the most recent failed {@link initWasm} attempt, or null. */\nexport function getWasmInitError(): unknown {\n return lastInitError;\n}\n\nimport { resolveBasePath } from \"./basePath\";\n\n/**\n * Initialize the WASM engine. Must be called before any other function.\n * Safe to call multiple times \u2014 only initializes once.\n */\nexport async function initWasm(): Promise<void> {\n if (wasmModule) return;\n if (initPromise) return initPromise;\n\n const attempt = (async () => {\n try {\n const basePath = resolveBasePath();\n const assetHash =\n typeof __VIRITURA_WASM_ASSET_HASH__ === \"string\" && __VIRITURA_WASM_ASSET_HASH__.length > 0\n ? `.${__VIRITURA_WASM_ASSET_HASH__}`\n : \"\";\n\n // Step 1: Fetch the WASM binary\n const wasmResponse = await fetch(`${basePath}wasm/viritura_wasm_bg${assetHash}.wasm`);\n if (!wasmResponse.ok) {\n throw new Error(`Failed to fetch WASM binary: ${wasmResponse.status}`);\n }\n const wasmBytes = await wasmResponse.arrayBuffer();\n\n // Step 2: Import the same-origin generated glue directly. Avoiding a\n // blob: module lets production enforce `script-src 'self'`.\n const glue = await import(\n /* @vite-ignore */ /* webpackIgnore: true */ `${basePath}wasm/viritura_wasm${assetHash}.js`\n );\n\n // Step 3: Initialize with the binary bytes (avoids a second WASM fetch).\n const wasmModule_ = new WebAssembly.Module(wasmBytes);\n glue.initSync({ module: wasmModule_ });\n\n wasmModule = glue;\n lastInitError = null;\n _flushPendingDebugFlag();\n console.log(`Viritura WASM engine v${glue.engine_version()} loaded`);\n } catch (e) {\n lastInitError = e;\n console.warn(\"WASM engine failed to load:\", e);\n if (e instanceof Error) {\n console.warn(\"Error details:\", e.message);\n }\n wasmModule = null;\n }\n })();\n initPromise = attempt;\n try {\n await attempt;\n } finally {\n // A transient fetch/CSP/cache failure must not poison initialization for\n // the lifetime of this realm. Successful initialization keeps the resolved\n // promise as the idempotent fast path; failure clears it so a later caller\n // can retry.\n if (!wasmModule && initPromise === attempt) initPromise = null;\n }\n}\n\n/**\n * Check if the WASM engine is loaded and ready.\n */\nexport function isWasmReady(): boolean {\n return wasmModule !== null;\n}\n\n/** Semantic version reported by the loaded WASM engine, or null before init. */\nexport function getEngineVersion(): string | null {\n return wasmModule ? wasmModule.engine_version() : null;\n}\n\nexport function computeSlurPreview(input: SlurPreviewInput): SlurPreview {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n return JSON.parse(wasmModule.compute_slur_preview(JSON.stringify(input))) as SlurPreview;\n}\n\n/** Older/uninitialized WASM can still show the cursor, but cannot engrave a ghost. */\nexport function computeNotePreview(input: NotePreviewInput): RenderCommand[] {\n if (!wasmModule?.compute_note_preview) return [];\n return JSON.parse(wasmModule.compute_note_preview(JSON.stringify(input))) as RenderCommand[];\n}\n\n/**\n * Compute layout using the WASM engine.\n * Returns a DisplayList of render commands.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\n/**\n * Map snake_case keys from Rust JSON to camelCase TypeScript DisplayList.\n * Handles: element_ids \u2192 elementIds, element_bboxes \u2192 elementBboxes\n */\nfunction normalizeDisplayList(raw: Record<string, unknown>): DisplayList {\n const dl = raw as unknown as DisplayList;\n // Remap snake_case \u2192 camelCase if needed\n if (!dl.elementIds && (raw as Record<string, unknown>)[\"element_ids\"]) {\n dl.elementIds = (raw as Record<string, unknown>)[\"element_ids\"] as (string | null)[];\n }\n if (!dl.elementBboxes && (raw as Record<string, unknown>)[\"element_bboxes\"]) {\n const rawBboxes = (raw as Record<string, unknown>)[\"element_bboxes\"] as Record<string, unknown>[];\n dl.elementBboxes = rawBboxes.map((b) => ({\n elementId: (b[\"elementId\"] ?? b[\"element_id\"]) as string,\n bbox: (b[\"bbox\"] ?? b) as BoundingBox,\n }));\n }\n if (!dl.selectionGroups && (raw as Record<string, unknown>)[\"selection_groups\"]) {\n const rawGroups = (raw as Record<string, unknown>)[\"selection_groups\"] as Record<string, unknown>[];\n dl.selectionGroups = rawGroups.map((group) => ({\n elementId: (group[\"elementId\"] ?? group[\"element_id\"]) as string,\n memberIds: (group[\"memberIds\"] ?? group[\"member_ids\"]) as string[],\n }));\n }\n // Normalize measure_bounds \u2192 measureBounds\n if (!dl.measureBounds && (raw as Record<string, unknown>)[\"measure_bounds\"]) {\n const rawBounds = (raw as Record<string, unknown>)[\"measure_bounds\"] as Record<string, unknown>[];\n dl.measureBounds = rawBounds.map((b) => ({\n index: b[\"index\"] as number,\n measureId: (b[\"measureId\"] ?? b[\"measure_id\"]) as string | undefined,\n partIndex: (b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n partId: (b[\"partId\"] ?? b[\"part_id\"]) as string,\n sourcePartIndices: (b[\"sourcePartIndices\"] ?? b[\"source_part_indices\"]) as number[] | undefined,\n staffIndex: (b[\"staffIndex\"] ?? b[\"staff_index\"] ?? b[\"partIndex\"] ?? b[\"part_index\"]) as number,\n systemIndex: (b[\"systemIndex\"] ?? b[\"system_index\"]) as number | undefined,\n x: b[\"x\"] as number,\n width: b[\"width\"] as number,\n y: b[\"y\"] as number,\n height: b[\"height\"] as number,\n prefixWidth: (b[\"prefixWidth\"] ?? b[\"prefix_width\"]) as number,\n totalBeats: (b[\"totalBeats\"] ?? b[\"total_beats\"]) as number,\n beatAnchors: (b[\"beatAnchors\"] ?? b[\"beat_anchors\"]) as [number, number][],\n ghostStaff: (b[\"ghostStaff\"] ?? b[\"ghost_staff\"]) as boolean | undefined,\n isHidden: (b[\"isHidden\"] ?? b[\"is_hidden\"]) as boolean | undefined,\n hasMusicHidden: (b[\"hasMusicHidden\"] ?? b[\"has_music_hidden\"]) as boolean | undefined,\n isExpansion: (b[\"isExpansion\"] ?? b[\"is_expansion\"]) as boolean | undefined,\n }));\n }\n // Normalize layout_debug \u2192 layoutDebug\n if (!dl.layoutDebug && (raw as Record<string, unknown>)[\"layout_debug\"]) {\n dl.layoutDebug = (raw as Record<string, unknown>)[\"layout_debug\"] as DisplayList[\"layoutDebug\"];\n }\n // Normalize slur_geometries \u2192 slurGeometries\n if (!dl.slurGeometries && (raw as Record<string, unknown>)[\"slur_geometries\"]) {\n const rawSlurs = (raw as Record<string, unknown>)[\"slur_geometries\"] as Record<string, unknown>[];\n dl.slurGeometries = rawSlurs.map((s) => ({\n elementId: (s[\"elementId\"] ?? s[\"element_id\"]) as string,\n p0x: (s[\"p0x\"] ?? s[\"p0_x\"]) as number,\n p0y: (s[\"p0y\"] ?? s[\"p0_y\"]) as number,\n p1x: (s[\"p1x\"] ?? s[\"p1_x\"]) as number,\n p1y: (s[\"p1y\"] ?? s[\"p1_y\"]) as number,\n p2x: (s[\"p2x\"] ?? s[\"p2_x\"]) as number,\n p2y: (s[\"p2y\"] ?? s[\"p2_y\"]) as number,\n p3x: (s[\"p3x\"] ?? s[\"p3_x\"]) as number,\n p3y: (s[\"p3y\"] ?? s[\"p3_y\"]) as number,\n thickness: s[\"thickness\"] as number,\n curveDir: (s[\"curveDir\"] ?? s[\"curve_dir\"]) as number,\n sp: s[\"sp\"] as number,\n }));\n }\n // Normalize page_turn_warnings \u2192 pageTurnWarnings\n if (!dl.pageTurnWarnings && (raw as Record<string, unknown>)[\"page_turn_warnings\"]) {\n const rawWarnings = (raw as Record<string, unknown>)[\"page_turn_warnings\"] as Record<string, unknown>[];\n dl.pageTurnWarnings = rawWarnings.map((w) => ({\n boundaryMeasure: (w[\"boundaryMeasure\"] ?? w[\"boundary_measure\"]) as number,\n kind: w[\"kind\"] as string,\n turnSeconds: (w[\"turnSeconds\"] ?? w[\"turn_seconds\"]) as number,\n }));\n }\n return dl;\n}\n\nexport function computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_layout(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout for all parts stacked vertically using the WASM engine.\n * Returns a DisplayList of render commands for the full score.\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_full_score_layout(mnxJson, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Compute layout using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeLayoutBinary(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_layout_binary(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout for all parts using the WASM engine, returning binary Float32Array.\n * Significantly faster than JSON for large scores (>50 measures).\n * @param pageWidth - Available page width in pixels (0 = no system breaking)\n */\nexport function computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_full_score_layout_binary(mnxJson, spatium, pageWidth, pageSetupJson);\n}\n\n/**\n * Compute layout using a specific MNX score definition index.\n * Returns a DisplayList of render commands.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): DisplayList {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.compute_mnx_score_layout(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n}\n\n/**\n * Binary sibling of {@link computeMnxScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it to the main\n * thread zero-copy instead of structured-cloning a deep DisplayList.\n * @param scoreIndex - Index into the MNX scores[] array\n */\nexport function computeMnxScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth: number = 0,\n scoreIndex: number = 0,\n pageSetupJson?: string,\n): Float32Array {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n return wasmModule.compute_mnx_score_layout_binary(mnxJson, spatium, pageWidth, scoreIndex, pageSetupJson);\n}\n\n/**\n * Get score metadata from MNX JSON.\n */\nexport function getScoreInfo(mnxJson: string): ScoreInfo {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.get_score_info(mnxJson);\n return JSON.parse(resultJson) as ScoreInfo;\n}\n\n/**\n * Toggle the layout-debug sidecar. When enabled, every subsequent layout\n * call emits a `layoutDebug` field on the returned `DisplayList`. Used by\n * the editor's spacing debug overlay.\n *\n * Safe to call before `initWasm()` resolves \u2014 the value is replayed onto\n * the module once it's ready.\n */\nlet pendingDebugFlag: boolean | null = null;\nexport function setEmitLayoutDebug(enabled: boolean): void {\n if (!wasmModule) {\n pendingDebugFlag = enabled;\n return;\n }\n if (typeof wasmModule.set_emit_layout_debug === \"function\") {\n wasmModule.set_emit_layout_debug(enabled);\n }\n}\n\n/** Internal \u2014 called from initWasm once the module is ready. */\nfunction _flushPendingDebugFlag(): void {\n if (pendingDebugFlag !== null && wasmModule?.set_emit_layout_debug) {\n wasmModule.set_emit_layout_debug(pendingDebugFlag);\n pendingDebugFlag = null;\n }\n}\n\nexport function getEmitLayoutDebug(): boolean {\n return Boolean(wasmModule?.get_emit_layout_debug?.());\n}\n\n/** Per-page SVG result from WASM export. */\nexport interface WasmSvgPage {\n pageNumber: number;\n svg: string;\n widthMm: number;\n heightMm: number;\n}\n\n/**\n * Export a DisplayList to per-page SVG strings via the WASM engine.\n *\n * All music glyphs and text are expanded to vector path outlines using\n * font data, producing self-contained SVGs with no external dependencies.\n *\n * @param displayListJson - Serialised DisplayList (from any compute_* function).\n * @param bravuraData - Raw bytes of Bravura.otf (SMuFL music font).\n * @param textFontData - Raw bytes of a text font (e.g. BravuraText.otf). Pass empty for fallback.\n * @returns Array of per-page SVG results.\n */\nexport function wasmExportSvg(\n displayListJson: string,\n bravuraData: Uint8Array,\n textFontData: Uint8Array,\n spatiumMm: number,\n spPixels: number,\n pageWidthMm: number,\n pageHeightMm: number,\n): WasmSvgPage[] {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n const resultJson = wasmModule.export_svg(\n displayListJson,\n bravuraData,\n textFontData,\n spatiumMm,\n spPixels,\n pageWidthMm,\n pageHeightMm,\n );\n return JSON.parse(resultJson) as WasmSvgPage[];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Stateful cached layout engine\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Opaque handle to a WASM LayoutEngine instance with measure-level cache. */\ninterface WasmLayoutEngine {\n compute_layout_cached(\n mnx_json: string,\n part_index: number,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n ): string;\n compute_full_score_layout_cached(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n compute_full_score_layout_cached_binary(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n relayout_retained_score_cached(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n relayout_retained_score_cached_binary(\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n apply_patch_and_layout_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n apply_patch_and_layout_patch_frame_binary(\n patch_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): Float32Array;\n full_layout(\n mnx_json: string,\n spatium: number,\n page_width: number,\n page_setup_json?: string,\n score_index?: number,\n ): string;\n invalidate_cache(): void;\n cache_stats(): number[];\n layout_metrics_json(): string;\n take_timings_json(): string | undefined;\n has_retained_score(): boolean;\n /**\n * Lever 2 step 4 (B-full): enable the per-system wholesale layout-reuse\n * store. Off by default in the engine; the factory opts in. Byte-identical\n * to the per-measure path (proven by the engine oracle).\n */\n set_system_layout_reuse?(enabled: boolean): void;\n free(): void;\n}\n\n/** Enable detailed engine phase timings in this realm's WASM instance. */\nexport function setWasmTiming(enabled: boolean): void {\n wasmModule?.set_wasm_timing?.(enabled);\n}\n\n/** Deterministic counters from the most recent retained-engine layout pass. */\nexport interface LayoutMetrics {\n resolvedCells: number;\n resolvedFullCells: number;\n widthCells: number;\n widthFullCells: number;\n freshSystems: number;\n reusedSystems: number;\n staffContentReuses: number;\n staffContentReuseRuns: number;\n staffAuxReuses: number;\n systemMeasureReuses: number;\n spannerBoundsFull: number;\n spannerBounds: number;\n mmrPlanReused: boolean;\n frameBytes: number;\n patchFrame: boolean;\n horizonChunksReused: boolean;\n horizonStaffExtentsReused: number;\n horizonTieMapsReused: number;\n cacheHits: number;\n cacheMisses: number;\n}\n\nexport const EMPTY_LAYOUT_METRICS: LayoutMetrics = {\n resolvedCells: 0,\n resolvedFullCells: 0,\n widthCells: 0,\n widthFullCells: 0,\n freshSystems: 0,\n reusedSystems: 0,\n staffContentReuses: 0,\n staffContentReuseRuns: 0,\n staffAuxReuses: 0,\n systemMeasureReuses: 0,\n spannerBoundsFull: 0,\n spannerBounds: 0,\n mmrPlanReused: false,\n frameBytes: 0,\n patchFrame: false,\n horizonChunksReused: false,\n horizonStaffExtentsReused: 0,\n horizonTieMapsReused: 0,\n cacheHits: 0,\n cacheMisses: 0,\n};\n\n/**\n * Stateful layout engine with measure-level cache.\n * Reuses natural width computations for unchanged measures across re-layouts.\n *\n * Usage:\n * ```ts\n * const engine = createCachedLayoutEngine();\n * const dl1 = engine.computeLayout(mnx, 0, sp, pw); // full computation\n * const dl2 = engine.computeLayout(mnx2, 0, sp, pw); // only changed measures recomputed\n * engine.dispose();\n * ```\n */\nexport interface CachedLayoutEngine {\n computeLayout(\n mnxJson: string,\n partIndex: number,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n ): DisplayList;\n computeFullScoreLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link computeFullScoreLayout}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy,\n * avoiding a multi-megabyte JSON serialize + clone + parse on initial load. */\n computeFullScoreLayoutBinary(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Re-layout the already-retained Score for a different score view / config\n * WITHOUT re-parsing MNX JSON. Requires a prior full layout call; returns\n * null when no score is retained so the caller can fall back. */\n relayoutRetainedScore(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList | null;\n /** Binary sibling of {@link relayoutRetainedScore}: returns the raw packed\n * Float32Array (no decode) so a worker host can `transfer` it zero-copy.\n * Returns null when no score is retained. */\n relayoutRetainedScoreBinary(\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array | null;\n /** Apply a measure-level patch and re-layout incrementally.\n * Only changed measures are re-parsed. Requires a prior full layout call. */\n applyPatchAndLayout(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n /** Binary sibling of {@link applyPatchAndLayout}: returns the raw packed\n * Float32Array (no decode) for zero-copy worker transfer. */\n applyPatchAndLayoutBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Incremental sibling of {@link applyPatchAndLayoutBinary} that returns a\n * PATCH-FRAME delta (tagged at element 0: `0.0` = full display list, `1.0` =\n * patch frame) instead of a full display list, when the paged auto-flow path\n * can produce one. The caller reassembles it against a retained per-system\n * segment list (see `PatchReconstructor`). Falls back to a tagged full frame\n * transparently for every other layout path. */\n applyPatchAndLayoutPatchFrameBinary(\n patchJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): Float32Array;\n /** Full parse + layout with cache invalidation (for structural changes). */\n fullLayout(\n mnxJson: string,\n spatium: number,\n pageWidth?: number,\n pageSetupJson?: string,\n scoreIndex?: number,\n ): DisplayList;\n invalidateCache(): void;\n /** Returns [hits, misses] from the last layout pass. */\n cacheStats(): [number, number];\n /** Deterministic work/transport counters from the last layout pass. */\n layoutMetrics(): LayoutMetrics;\n /** Drain the optional detailed phase timing JSON for the last patch. */\n takeTimings(): string | null;\n /** True if the engine has a retained Score (patch API available). */\n hasRetainedScore(): boolean;\n dispose(): void;\n}\n\n/**\n * Create a stateful cached layout engine.\n * Must call initWasm() first.\n */\nexport function createCachedLayoutEngine(): CachedLayoutEngine {\n if (!wasmModule) {\n throw new Error(\"WASM engine not initialized. Call initWasm() first.\");\n }\n\n // The LayoutEngine constructor is exposed via wasm_bindgen\n const EngineClass = (wasmModule as Record<string, unknown>)[\"LayoutEngine\"] as {\n new (): WasmLayoutEngine;\n };\n if (!EngineClass) {\n throw new Error(\"LayoutEngine not found in WASM module. Rebuild WASM.\");\n }\n const engine = new EngineClass();\n\n // Lever 2 step 4 (B-full): opt into the per-system wholesale layout-reuse\n // store. Byte-identical to the per-measure path (engine oracle), and ~18ms\n // faster per warm edit on large paged scores. Guarded with `?.` so an older\n // WASM build without the setter still loads.\n engine.set_system_layout_reuse?.(true);\n\n return {\n computeLayout(mnxJson, partIndex, spatium, pageWidth = 0, pageSetupJson?) {\n const resultJson = engine.compute_layout_cached(mnxJson, partIndex, spatium, pageWidth, pageSetupJson);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.compute_full_score_layout_cached(\n mnxJson,\n spatium,\n pageWidth,\n pageSetupJson,\n scoreIndex,\n );\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n computeFullScoreLayoutBinary(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.compute_full_score_layout_cached_binary(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n relayoutRetainedScore(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n const resultJson = engine.relayout_retained_score_cached(spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n relayoutRetainedScoreBinary(spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n if (!engine.has_retained_score()) return null;\n return engine.relayout_retained_score_cached_binary(spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayout(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n performance.mark(\"viritura:wasm-raw-start\");\n const binary = engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n performance.mark(\"viritura:wasm-raw-end\");\n performance.measure(\"viritura:wasm-raw\", \"viritura:wasm-raw-start\", \"viritura:wasm-raw-end\");\n\n performance.mark(\"viritura:displaylist-parse-start\");\n const normalized = decodeBinaryDisplayList(binary);\n performance.mark(\"viritura:displaylist-parse-end\");\n performance.measure(\n \"viritura:displaylist-parse\",\n \"viritura:displaylist-parse-start\",\n \"viritura:displaylist-parse-end\",\n );\n return normalized;\n },\n applyPatchAndLayoutBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n applyPatchAndLayoutPatchFrameBinary(patchJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n return engine.apply_patch_and_layout_patch_frame_binary(patchJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n },\n fullLayout(mnxJson, spatium, pageWidth = 0, pageSetupJson?, scoreIndex?) {\n const resultJson = engine.full_layout(mnxJson, spatium, pageWidth, pageSetupJson, scoreIndex);\n return normalizeDisplayList(JSON.parse(resultJson));\n },\n invalidateCache() {\n engine.invalidate_cache();\n },\n cacheStats(): [number, number] {\n const stats = engine.cache_stats();\n return [stats[0] ?? 0, stats[1] ?? 0];\n },\n layoutMetrics(): LayoutMetrics {\n try {\n return JSON.parse(engine.layout_metrics_json()) as LayoutMetrics;\n } catch {\n return { ...EMPTY_LAYOUT_METRICS };\n }\n },\n takeTimings(): string | null {\n return engine.take_timings_json() ?? null;\n },\n hasRetainedScore() {\n return engine.has_retained_score();\n },\n dispose() {\n engine.free();\n },\n };\n}\n", "/**\n * Tile-based rendering cache for score canvases.\n *\n * Pre-renders the display list into fixed-size tiles at the current zoom level.\n * On scroll, blits cached tiles instead of repainting all render commands.\n * Tiles are invalidated when zoom or content changes.\n *\n * Supports dirty tile invalidation: when content changes, only tiles overlapping\n * modified measures are invalidated (via invalidateMeasures), not the entire cache.\n *\n * Performance: reduces per-frame paint from O(commands) to O(visible tiles).\n * For a 32-bar orchestral score, that's ~1500 draw calls \u2192 ~6 drawImage blits.\n */\n\nimport { paintCommand } from \"./displayListPainter\";\nimport { computeHorizonPaperGeometry, paintPaperPage, PAPER_SHADOW_MARGIN } from \"./paperPagePainter\";\nimport { splitCommandsByPage } from \"./pageCache\";\nimport type { DisplayList, RenderCommand, MeasureBounds } from \"./wasm\";\nimport type { GlyphAtlas } from \"./glyphAtlas\";\nimport { renderCommandBounds } from \"./renderCommandBounds\";\n\n/** Default tile size in CSS pixels. Larger = fewer tiles but more memory per tile. */\nexport const DEFAULT_TILE_SIZE = 512;\n\n/**\n * Width (in content px) of one horizontal command bucket for horizon view.\n * Horizon emits a single unbroken system that can be ~190k px wide; without a\n * spatial split every tile would re-scan the entire command list (O(tiles \u00D7\n * commands)). Bucketing along X lets each tile iterate only the commands whose\n * X-extent overlaps the visible band, mirroring how page view splits commands\n * by page along Y. Chosen so a tile spans only a handful of buckets across the\n * usual zoom range (a 512px tile is ~1.7k content px at the default 0.3 zoom).\n */\nconst HORIZON_BUCKET_WIDTH = 2048;\n\n/** Max cached tiles before distant ones are evicted (bounds memory for very wide horizon scores). */\nconst MAX_CACHED_TILES = 512;\n\n/** Column buffer kept around the visible range when evicting distant tiles. */\nconst TILE_EVICT_BUFFER = 8;\n\n/**\n * Horizontal spatial index over a display list's commands for horizon view.\n * `buckets[b]` holds the ascending global indices of commands whose X-extent\n * overlaps band `[minX + b*bucketWidth, minX + (b+1)*bucketWidth)`. Commands\n * spanning multiple bands appear in each overlapping bucket. Position-less\n * commands (SetOpacity \u2014 order-dependent global state) are collected separately\n * in `opacityIndices` and merged into every tile so alpha state stays correct.\n */\ninterface HorizonIndex {\n bucketWidth: number;\n minX: number;\n buckets: number[][];\n opacityIndices: number[];\n}\n\n/**\n * K-way merge of the sorted index arrays for buckets `b1..b2` plus `extra`\n * (opacity indices), producing a single ascending, de-duplicated index list so\n * commands paint in their original global order with no double-draw.\n */\nfunction mergeSortedUnique(buckets: number[][], b1: number, b2: number, extra: number[]): number[] {\n const lists: number[][] = [];\n for (let b = b1; b <= b2; b++) {\n const l = buckets[b];\n if (l && l.length > 0) lists.push(l);\n }\n if (extra.length > 0) lists.push(extra);\n if (lists.length === 0) return [];\n if (lists.length === 1) return lists[0]!;\n\n const pointers = new Array<number>(lists.length).fill(0);\n const out: number[] = [];\n let last = -1;\n for (;;) {\n let minVal = Infinity;\n let minList = -1;\n for (let k = 0; k < lists.length; k++) {\n const list = lists[k]!;\n const p = pointers[k]!;\n if (p < list.length && list[p]! < minVal) {\n minVal = list[p]!;\n minList = k;\n }\n }\n if (minList === -1) break;\n pointers[minList]!++;\n if (minVal !== last) {\n out.push(minVal);\n last = minVal;\n }\n }\n return out;\n}\n\n/**\n * Compute the actual bounding box of all render commands, extended past the\n * nominal `displayList.width`/`height` and padded so edge text and the paper\n * shadow halo aren't clipped.\n *\n * This is the single source of truth for \"how far does painted content\n * actually reach\". The tile renderer uses it to decide which tiles exist, and\n * the editor's scroll clamp uses it (via `contentSizeForMode`) so the viewport\n * can scroll to every painted pixel. Keeping both on the same function avoids\n * the bug where the scroll clamp stopped short of content that overhangs the\n * nominal dimensions (most visible at high zoom on wide horizon scores).\n */\nexport function computeDisplayListContentBounds(displayList: DisplayList): {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n} {\n let minX = 0;\n let minY = 0;\n let maxX = displayList.width;\n let maxY = displayList.height;\n\n for (const cmd of displayList.commands) {\n const b = renderCommandBounds(cmd);\n if (!b) continue;\n if (b.x < minX) minX = b.x;\n if (b.y < minY) minY = b.y;\n if (b.x2 > maxX) maxX = b.x2;\n if (b.y2 > maxY) maxY = b.y2;\n }\n\n // Generous padding so text at edges isn't clipped AND the paper shadow\n // halo gets rendered into tiles outside the page bounds. Without this,\n // the shadow falls into unallocated tiles and is invisible in\n // tile-cached mode.\n const pad = PAPER_SHADOW_MARGIN + 8;\n return { minX: minX - pad, minY: minY - pad, maxX: maxX + pad, maxY: maxY + pad };\n}\n\n/** Key for a tile at (col, row). */\nfunction tileKey(col: number, row: number): string {\n return `${col},${row}`;\n}\n\ninterface TileRenderOpts {\n displayList: DisplayList;\n zoom: number;\n dpr: number;\n glyphAtlas: GlyphAtlas | null;\n viewMode: \"page\" | \"spread\" | \"spread-h\" | \"horizon\";\n canvasBg: string;\n paperFill: string | CanvasPattern | null;\n pageStackGap: number;\n printableInsets: PageInsets;\n}\n\ninterface PageInsets {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport interface PaintFrameOpts {\n canvas: HTMLCanvasElement;\n displayList: DisplayList;\n scrollX: number;\n scrollY: number;\n zoom: number;\n version: number;\n glyphAtlas: GlyphAtlas | null;\n viewMode?: \"page\" | \"spread\" | \"spread-h\" | \"horizon\";\n canvasBg?: string;\n /** Page fill; \null paints ink only (no paper or shadow). */\n paperFill?: string | CanvasPattern | null;\n pageStackGap?: number;\n printableInsets?: PageInsets;\n}\n\nfunction paintHorizonPaper(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n paperFill: string | CanvasPattern | null,\n): void {\n if (viewMode !== \"horizon\" || paperFill === null) return;\n const paper = computeHorizonPaperGeometry(displayList);\n paintPaperPage(ctx, paper.x, paper.y, paper.width, paper.height, paperFill);\n}\n\nfunction paintPageBackgrounds(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n pageStackGap: number,\n paperFill: string | CanvasPattern | null,\n contentY: number,\n contentH: number,\n): void {\n if (viewMode !== \"page\" || paperFill === null || !displayList.pages || displayList.pages.length === 0) return;\n const pageW = displayList.width;\n for (let pi = 0; pi < displayList.pages.length; pi++) {\n const page = displayList.pages[pi];\n if (!page) continue;\n const visualY = page.yOffset + pi * pageStackGap;\n const pageBottom = visualY + page.height;\n if (pageBottom + PAPER_SHADOW_MARGIN < contentY || visualY - PAPER_SHADOW_MARGIN > contentY + contentH) continue;\n paintPaperPage(ctx, 0, visualY, pageW, page.height, paperFill);\n }\n}\n\n/**\n * Paint render commands, skipping those whose bounding box lies fully outside\n * the cull rectangle. Commands without a bounding box (e.g. `SetOpacity`) carry\n * order-dependent global state and are always painted so opacity stays correct\n * regardless of which commands get culled. Exported for direct-paint callers\n * (spread view, zoom/import frames) that bypass the tile cache.\n */\nexport function paintCommandsCulled(\n ctx: CanvasRenderingContext2D,\n commands: readonly RenderCommand[],\n glyphAtlas: GlyphAtlas | null,\n cullX1: number,\n cullX2: number,\n cullY1: number,\n cullY2: number,\n): void {\n for (const cmd of commands) {\n const b = renderCommandBounds(cmd);\n if (b && (b.x2 < cullX1 || b.x > cullX2 || b.y2 < cullY1 || b.y > cullY2)) continue;\n if (cmd.type === \"DrawGlyph\" && glyphAtlas?.isBuilt) {\n const drawn = glyphAtlas.drawGlyph(ctx, cmd.codepoint, cmd.x, cmd.y, cmd.size, cmd.color);\n if (drawn) continue;\n }\n paintCommand(ctx, cmd);\n }\n}\n\nexport class TileCache {\n private tiles = new Map<string, HTMLCanvasElement>();\n private tileSize: number;\n private cachedVersion = -1;\n private cachedDisplayList: DisplayList | null = null;\n private cachedZoom = -1;\n private cachedDpr = -1;\n private cachedViewMode: \"page\" | \"spread\" | \"spread-h\" | \"horizon\" = \"page\";\n private cachedCanvasBg = \"\";\n /** Last paper fill (cream string or CanvasPattern) used to render tiles.\n * Identity comparison \u2014 a CanvasPattern object stays stable per theme,\n * and a transition from cream-string to pattern triggers re-render. */\n private cachedPaperFill: string | CanvasPattern | null = \"\";\n /** Last page-stack gap (px) used. Invalidates tiles on change. */\n private cachedPageStackGap = 0;\n private cachedPrintableInsets = \"\";\n\n /** Per-page command split for page view (rebuilt when version changes). */\n private pageCommands: RenderCommand[][] | null = null;\n\n /** Horizontal command index for horizon view (rebuilt when version changes). */\n private horizonBuckets: HorizonIndex | null = null;\n\n /** Previous measure bounds for dirty tile detection. */\n private prevMeasureBounds: readonly MeasureBounds[] = [];\n\n /** Cached actual content bounds (min/max across all commands). */\n private contentBounds = { minX: 0, minY: 0, maxX: 0, maxY: 0 };\n\n /** Max new tiles to render per frame. Remaining tiles render in subsequent frames. */\n private maxNewTilesPerFrame = 24;\n\n /** Number of tiles rendered in the last paintFrame call. */\n tilesRendered = 0;\n /** Number of tiles blitted from cache in the last paintFrame call. */\n tilesCached = 0;\n /** True if there are uncached visible tiles that weren't rendered this frame. */\n hasPendingTiles = false;\n\n constructor(tileSize = DEFAULT_TILE_SIZE) {\n this.tileSize = tileSize;\n }\n\n /** Invalidate all cached tiles (e.g. on zoom change). */\n invalidate(): void {\n this.tiles.clear();\n }\n\n /** Compute the actual bounding box of all render commands, with padding. */\n private computeContentBounds(displayList: DisplayList): { minX: number; minY: number; maxX: number; maxY: number } {\n return computeDisplayListContentBounds(displayList);\n }\n\n /**\n * Invalidate only tiles overlapping changed measures.\n * Compares old and new MeasureBounds by measure_id to find dirty regions,\n * then deletes only those tiles that overlap the changed rectangles.\n */\n invalidateMeasures(oldBounds: readonly MeasureBounds[], newBounds: readonly MeasureBounds[], zoom: number): void {\n // Build map of old bounds by measure_id+partIndex for comparison\n const oldMap = new Map<string, MeasureBounds>();\n for (const mb of oldBounds) {\n const key = `${mb.measureId ?? mb.index}:${mb.partIndex}`;\n oldMap.set(key, mb);\n }\n\n // Find dirty rectangles: measures that changed position, size, or are new/removed\n const dirtyRects: Array<{ x: number; y: number; x2: number; y2: number }> = [];\n\n for (const mb of newBounds) {\n const key = `${mb.measureId ?? mb.index}:${mb.partIndex}`;\n const old = oldMap.get(key);\n if (!old || old.x !== mb.x || old.y !== mb.y || old.width !== mb.width || old.height !== mb.height) {\n // This measure changed \u2014 mark both old and new rectangles as dirty\n dirtyRects.push({ x: mb.x, y: mb.y, x2: mb.x + mb.width, y2: mb.y + mb.height });\n if (old) {\n dirtyRects.push({ x: old.x, y: old.y, x2: old.x + old.width, y2: old.y + old.height });\n }\n }\n oldMap.delete(key);\n }\n\n // Any remaining old entries were deleted measures\n for (const old of oldMap.values()) {\n dirtyRects.push({ x: old.x, y: old.y, x2: old.x + old.width, y2: old.y + old.height });\n }\n\n if (dirtyRects.length === 0) return;\n\n // Delete tiles overlapping any dirty rectangle\n const ts = this.tileSize;\n const keysToDelete: string[] = [];\n for (const [key, _tile] of this.tiles) {\n const [colStr, rowStr] = key.split(\",\");\n const col = parseInt(colStr!, 10);\n const row = parseInt(rowStr!, 10);\n // Tile covers content rect\n const tileX1 = (col * ts) / zoom;\n const tileY1 = (row * ts) / zoom;\n const tileX2 = ((col + 1) * ts) / zoom;\n const tileY2 = ((row + 1) * ts) / zoom;\n\n for (const r of dirtyRects) {\n if (tileX2 > r.x && tileX1 < r.x2 && tileY2 > r.y && tileY1 < r.y2) {\n keysToDelete.push(key);\n break;\n }\n }\n }\n\n for (const key of keysToDelete) {\n this.tiles.delete(key);\n }\n }\n\n /**\n * Render a single tile at (col, row) in tile-grid coordinates.\n */\n private renderTile(col: number, row: number, opts: TileRenderOpts): HTMLCanvasElement {\n const { displayList, zoom, dpr, glyphAtlas, viewMode, canvasBg, paperFill, pageStackGap, printableInsets } = opts;\n const ts = this.tileSize;\n const deviceTs = Math.round(ts * dpr);\n const canvas = document.createElement(\"canvas\");\n canvas.width = deviceTs;\n canvas.height = deviceTs;\n const ctx = canvas.getContext(\"2d\")!;\n\n ctx.fillStyle = canvasBg;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n const contentX = (col * ts) / zoom;\n const contentY = (row * ts) / zoom;\n ctx.setTransform(dpr * zoom, 0, 0, dpr * zoom, -contentX * dpr * zoom, -contentY * dpr * zoom);\n\n const contentW = ts / zoom;\n const contentH = ts / zoom;\n ctx.save();\n ctx.beginPath();\n ctx.rect(contentX, contentY, contentW, contentH);\n ctx.clip();\n\n paintHorizonPaper(ctx, displayList, viewMode, paperFill);\n paintPageBackgrounds(ctx, displayList, viewMode, pageStackGap, paperFill, contentY, contentH);\n\n const margin = 20 / zoom;\n const usePagePaint = viewMode === \"page\" && (displayList.pages?.length ?? 0) > 0;\n if (usePagePaint && this.pageCommands) {\n this.paintStackedPageCommands(\n ctx,\n displayList,\n glyphAtlas,\n contentX,\n contentY,\n contentW,\n contentH,\n margin,\n pageStackGap,\n printableInsets,\n );\n } else if (viewMode === \"horizon\" && this.horizonBuckets) {\n this.paintHorizonBucketed(ctx, displayList, glyphAtlas, contentX, contentY, contentW, contentH, margin);\n } else {\n this.paintFlatCommands(ctx, displayList, glyphAtlas, contentX, contentY, contentW, contentH, margin);\n }\n\n ctx.restore();\n return canvas;\n }\n\n /**\n * Build the horizontal command index for horizon view. Must be called after\n * {@link computeContentBounds} has populated {@link contentBounds}.\n */\n private buildHorizonBuckets(displayList: DisplayList): HorizonIndex {\n const { minX, maxX } = this.contentBounds;\n const bucketWidth = HORIZON_BUCKET_WIDTH;\n const span = Math.max(1, maxX - minX);\n const bucketCount = Math.max(1, Math.ceil(span / bucketWidth));\n const buckets: number[][] = Array.from({ length: bucketCount }, () => []);\n const opacityIndices: number[] = [];\n const commands = displayList.commands;\n for (let i = 0; i < commands.length; i++) {\n const b = renderCommandBounds(commands[i]!);\n if (!b) {\n // Position-less (SetOpacity): order-dependent global state, kept apart\n // and merged into every tile so alpha is correct regardless of band.\n opacityIndices.push(i);\n continue;\n }\n let b1 = Math.floor((b.x - minX) / bucketWidth);\n let b2 = Math.floor((b.x2 - minX) / bucketWidth);\n if (b1 < 0) b1 = 0;\n if (b1 > bucketCount - 1) b1 = bucketCount - 1;\n if (b2 < 0) b2 = 0;\n if (b2 > bucketCount - 1) b2 = bucketCount - 1;\n for (let k = b1; k <= b2; k++) buckets[k]!.push(i);\n }\n return { bucketWidth, minX, buckets, opacityIndices };\n }\n\n /**\n * Paint one horizon tile using only the commands whose horizontal band\n * overlaps the tile, instead of scanning the entire command list.\n */\n private paintHorizonBucketed(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n const cullY1 = contentY - margin;\n const cullY2 = contentY + contentH + margin;\n const { bucketWidth, minX, buckets, opacityIndices } = this.horizonBuckets!;\n const lastBucket = buckets.length - 1;\n let b1 = Math.floor((cullX1 - minX) / bucketWidth);\n let b2 = Math.floor((cullX2 - minX) / bucketWidth);\n if (b1 < 0) b1 = 0;\n if (b2 > lastBucket) b2 = lastBucket;\n if (b2 < 0 || b1 > lastBucket) return;\n\n const indices = mergeSortedUnique(buckets, b1, b2, opacityIndices);\n if (indices.length === 0) return;\n const commands = displayList.commands;\n const tileCmds = new Array<RenderCommand>(indices.length);\n for (let j = 0; j < indices.length; j++) tileCmds[j] = commands[indices[j]!]!;\n paintCommandsCulled(ctx, tileCmds, glyphAtlas, cullX1, cullX2, cullY1, cullY2);\n }\n\n private paintStackedPageCommands(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n pageStackGap: number,\n printableInsets: PageInsets,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n for (let pi = 0; pi < this.pageCommands!.length; pi++) {\n const cmds = this.pageCommands![pi];\n if (!cmds || cmds.length === 0) continue;\n const dy = pi * pageStackGap;\n const page = displayList.pages?.[pi];\n if (!page) continue;\n const pageCullY1 = contentY - margin - dy;\n const pageCullY2 = contentY + contentH + margin - dy;\n ctx.save();\n ctx.beginPath();\n ctx.rect(0, page.yOffset + dy, displayList.width, Math.max(0, page.height - printableInsets.bottom));\n ctx.clip();\n ctx.translate(0, dy);\n paintCommandsCulled(ctx, cmds, glyphAtlas, cullX1, cullX2, pageCullY1, pageCullY2);\n ctx.restore();\n }\n }\n\n private paintFlatCommands(\n ctx: CanvasRenderingContext2D,\n displayList: DisplayList,\n glyphAtlas: GlyphAtlas | null,\n contentX: number,\n contentY: number,\n contentW: number,\n contentH: number,\n margin: number,\n ): void {\n const cullX1 = contentX - margin;\n const cullX2 = contentX + contentW + margin;\n const cullY1 = contentY - margin;\n const cullY2 = contentY + contentH + margin;\n paintCommandsCulled(ctx, displayList.commands, glyphAtlas, cullX1, cullX2, cullY1, cullY2);\n }\n\n /**\n * Paint the visible portion of the score onto the main canvas using cached tiles.\n * Call this instead of repaintCanvas for tile-cached rendering.\n *\n * @param opts.canvasBg - Canvas background color (read from CSS variable by caller)\n * @param opts.paperFill - Page fill (cream solid or CanvasPattern). A change in\n * identity invalidates the cache so tiles get re-rendered with the new\n * paper texture when it finishes loading.\n */\n paintFrame(opts: PaintFrameOpts): void {\n const {\n canvas,\n displayList,\n scrollX,\n scrollY,\n zoom,\n version,\n glyphAtlas,\n viewMode = \"page\",\n canvasBg = \"#e0e2ea\",\n paperFill = \"#FFFFFF\",\n pageStackGap = 0,\n printableInsets = { top: 0, right: 0, bottom: 0, left: 0 },\n } = opts;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n const dpr = window.devicePixelRatio || 1;\n\n // Tiles rasterize at `dpr * zoom`; bake the atlas at the same scale so\n // glyph blits are 1:1 with device pixels instead of resampled (aliased).\n glyphAtlas?.ensureDeviceScale(dpr * zoom);\n\n this.maybeInvalidateForViewChange(zoom, viewMode, dpr, canvasBg, paperFill, pageStackGap, printableInsets);\n this.maybeInvalidateForVersionChange(version, displayList, viewMode);\n\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = canvasBg;\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.imageSmoothingEnabled = false;\n\n const range = this.computeVisibleTileRange(canvas, scrollX, scrollY, zoom, dpr, displayList, pageStackGap);\n this.blitVisibleTiles(\n ctx,\n range,\n {\n displayList,\n zoom,\n dpr,\n glyphAtlas,\n viewMode,\n canvasBg,\n paperFill,\n pageStackGap,\n printableInsets,\n },\n scrollX,\n scrollY,\n );\n }\n\n private maybeInvalidateForViewChange(\n zoom: number,\n viewMode: TileRenderOpts[\"viewMode\"],\n dpr: number,\n canvasBg: string,\n paperFill: string | CanvasPattern | null,\n pageStackGap: number,\n printableInsets: PageInsets,\n ): void {\n const printableInsetsKey = `${printableInsets.top},${printableInsets.right},${printableInsets.bottom},${printableInsets.left}`;\n if (\n zoom !== this.cachedZoom ||\n viewMode !== this.cachedViewMode ||\n dpr !== this.cachedDpr ||\n canvasBg !== this.cachedCanvasBg ||\n paperFill !== this.cachedPaperFill ||\n pageStackGap !== this.cachedPageStackGap ||\n printableInsetsKey !== this.cachedPrintableInsets\n ) {\n this.invalidate();\n this.cachedZoom = zoom;\n this.cachedViewMode = viewMode;\n this.cachedDpr = dpr;\n this.cachedCanvasBg = canvasBg;\n this.cachedPaperFill = paperFill;\n this.cachedPageStackGap = pageStackGap;\n this.cachedPrintableInsets = printableInsetsKey;\n }\n }\n\n private maybeInvalidateForVersionChange(\n version: number,\n displayList: DisplayList,\n viewMode: TileRenderOpts[\"viewMode\"],\n ): void {\n // The display list is published from an async WASM layout before React has\n // necessarily committed the matching version state. Treat object identity\n // as a second revision signal so that one paint with the previous numeric\n // version can never blit tiles from the previous list.\n if (version !== this.cachedVersion || displayList !== this.cachedDisplayList) {\n this.invalidate();\n this.prevMeasureBounds = displayList.measureBounds ?? [];\n this.cachedVersion = version;\n this.cachedDisplayList = displayList;\n this.contentBounds = this.computeContentBounds(displayList);\n this.pageCommands = viewMode === \"page\" ? splitCommandsByPage(displayList).map((p) => p.commands) : null;\n this.horizonBuckets = viewMode === \"horizon\" ? this.buildHorizonBuckets(displayList) : null;\n } else {\n if (viewMode === \"page\" && !this.pageCommands) {\n this.pageCommands = splitCommandsByPage(displayList).map((p) => p.commands);\n }\n if (viewMode === \"horizon\" && !this.horizonBuckets) {\n this.horizonBuckets = this.buildHorizonBuckets(displayList);\n }\n }\n }\n\n private computeVisibleTileRange(\n canvas: HTMLCanvasElement,\n scrollX: number,\n scrollY: number,\n zoom: number,\n dpr: number,\n displayList: DisplayList,\n pageStackGap: number,\n ): {\n minCol: number;\n maxCol: number;\n minRow: number;\n maxRow: number;\n startCol: number;\n endCol: number;\n startRow: number;\n endRow: number;\n } {\n const ts = this.tileSize;\n const viewW = canvas.width / dpr;\n const viewH = canvas.height / dpr;\n const startCol = Math.floor((scrollX * zoom) / ts);\n const startRow = Math.floor((scrollY * zoom) / ts);\n const endCol = Math.ceil(((scrollX + viewW / zoom) * zoom) / ts);\n const endRow = Math.ceil(((scrollY + viewH / zoom) * zoom) / ts);\n const stackExtra = pageStackGap > 0 && displayList.pages ? (displayList.pages.length - 1) * pageStackGap : 0;\n const { minX, minY, maxX, maxY } = this.contentBounds;\n const minCol = Math.floor((minX * zoom) / ts);\n const minRow = Math.floor((minY * zoom) / ts);\n const maxCol = Math.ceil((maxX * zoom) / ts);\n const maxRow = Math.ceil(((maxY + stackExtra) * zoom) / ts);\n return { minCol, maxCol, minRow, maxRow, startCol, endCol, startRow, endRow };\n }\n\n private blitVisibleTiles(\n ctx: CanvasRenderingContext2D,\n range: ReturnType<TileCache[\"computeVisibleTileRange\"]>,\n tileOpts: TileRenderOpts,\n scrollX: number,\n scrollY: number,\n ): void {\n const ts = this.tileSize;\n const { zoom, dpr } = tileOpts;\n this.tilesRendered = 0;\n this.tilesCached = 0;\n this.hasPendingTiles = false;\n\n for (let row = Math.max(range.minRow, range.startRow); row <= Math.min(range.maxRow, range.endRow); row++) {\n for (let col = Math.max(range.minCol, range.startCol); col <= Math.min(range.maxCol, range.endCol); col++) {\n const key = tileKey(col, row);\n let tile = this.tiles.get(key);\n if (!tile) {\n if (this.tilesRendered >= this.maxNewTilesPerFrame) {\n this.hasPendingTiles = true;\n continue;\n }\n tile = this.renderTile(col, row, tileOpts);\n this.tiles.set(key, tile);\n this.tilesRendered++;\n } else {\n this.tilesCached++;\n }\n const destX = Math.round(col * ts * dpr - scrollX * zoom * dpr);\n const destY = Math.round(row * ts * dpr - scrollY * zoom * dpr);\n ctx.drawImage(tile, destX, destY);\n }\n }\n\n this.evictDistantTiles(range);\n }\n\n /**\n * Bound memory for very wide scores (horizon can be ~190k px) by dropping\n * cached tiles whose column is far outside the visible range. Evicted tiles\n * simply re-render when scrolled back into view.\n */\n private evictDistantTiles(range: ReturnType<TileCache[\"computeVisibleTileRange\"]>): void {\n if (this.tiles.size <= MAX_CACHED_TILES) return;\n const keepMin = range.startCol - TILE_EVICT_BUFFER;\n const keepMax = range.endCol + TILE_EVICT_BUFFER;\n for (const key of this.tiles.keys()) {\n const comma = key.indexOf(\",\");\n const col = parseInt(key.slice(0, comma), 10);\n if (col < keepMin || col > keepMax) this.tiles.delete(key);\n }\n }\n}\n", "import { renderCommandBounds } from \"./renderCommandBounds\";\nimport type { DisplayList } from \"./wasm\";\n\n/**\n * Paint a paper-textured page rectangle using the canonical --paper-shadow\n * recipe from packages/ui/src/tokens.css. This is the same shadow stack\n * used on palette tiles, radial menu wedges, library cards, and the Paper\n * component \u2014 so the score canvas matches the rest of the surface system.\n *\n * CSS recipe (light theme):\n * 0 0.5px 1px rgba(80, 60, 30, 0.14)\n * 0 1.5px 3px -1 rgba(120, 90, 40, 0.14)\n * 0 4px 8px -3 rgba(140,110, 50, 0.10)\n * inset 0 1px 0 rgba(255,255,255,0.9) // lit rim\n * inset 0 0 0 1px rgba(120,100, 60, 0.10) // edge stroke\n *\n * Canvas 2D shadow API has no `spread`. We approximate a negative spread\n * by shrinking the rect that *casts* the shadow (the shadow footprint\n * gets smaller while staying centered), then paint the real page fill on\n * top with shadows disabled.\n */\n\ninterface ShadowLayer {\n offsetX: number;\n offsetY: number;\n blur: number;\n spread: number; // negative = footprint smaller than page rect\n color: string;\n}\n\ninterface PaperShadowRecipe {\n cast: ShadowLayer[];\n rimColor: string; // inset 0 1px 0\n edgeStrokeColor: string; // inset 0 0 0 1px\n}\n\nconst LIGHT_RECIPE: PaperShadowRecipe = {\n cast: [\n { offsetX: 0, offsetY: 0.5, blur: 1, spread: 0, color: \"rgba(80, 60, 30, 0.14)\" },\n { offsetX: 0, offsetY: 1.5, blur: 3, spread: -1, color: \"rgba(120, 90, 40, 0.14)\" },\n { offsetX: 0, offsetY: 4, blur: 8, spread: -3, color: \"rgba(140, 110, 50, 0.10)\" },\n ],\n rimColor: \"rgba(255, 255, 255, 0.9)\",\n edgeStrokeColor: \"rgba(120, 100, 60, 0.10)\",\n};\n\nconst DARK_RECIPE: PaperShadowRecipe = {\n cast: [\n { offsetX: 0, offsetY: 0.5, blur: 1, spread: 0, color: \"rgba(0, 0, 0, 0.40)\" },\n { offsetX: 0, offsetY: 1.5, blur: 3, spread: -1, color: \"rgba(20, 14, 4, 0.40)\" },\n { offsetX: 0, offsetY: 4, blur: 8, spread: -3, color: \"rgba(30, 20, 8, 0.32)\" },\n ],\n rimColor: \"rgba(255, 235, 200, 0.10)\",\n edgeStrokeColor: \"rgba(0, 0, 0, 0.35)\",\n};\n\n/** Bounding extent (px) the shadow paints past the page rect on each side. */\nexport const PAPER_SHADOW_MARGIN = 12;\nexport const HORIZON_PAPER_PADDING = 20;\n\nexport interface HorizonPaperGeometry {\n x: number;\n y: number;\n width: number;\n height: number;\n /** Symmetric vertical extent centered on the paper and its music ink. */\n contentHeight: number;\n}\n\ninterface InkBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\nfunction inkBounds(displayList: DisplayList): InkBounds | null {\n let minX = Number.POSITIVE_INFINITY;\n let minY = Number.POSITIVE_INFINITY;\n let maxX = Number.NEGATIVE_INFINITY;\n let maxY = Number.NEGATIVE_INFINITY;\n const preciseElementIds = new Set<string>();\n\n for (const element of displayList.elementBboxes ?? []) {\n preciseElementIds.add(element.elementId);\n minX = Math.min(minX, element.bbox.x);\n minY = Math.min(minY, element.bbox.y);\n maxX = Math.max(maxX, element.bbox.x + element.bbox.width);\n maxY = Math.max(maxY, element.bbox.y + element.bbox.height);\n }\n\n for (let index = 0; index < displayList.commands.length; index++) {\n const command = displayList.commands[index]!;\n const elementId = displayList.elementIds?.[index];\n const hasPreciseBounds = elementId != null && preciseElementIds.has(elementId);\n if (\n hasPreciseBounds &&\n (command.type === \"DrawGlyph\" || command.type === \"DrawStretchedGlyph\" || command.type === \"DrawText\")\n ) {\n continue;\n }\n const bounds = renderCommandBounds(command);\n if (!bounds) continue;\n minX = Math.min(minX, bounds.x);\n minY = Math.min(minY, bounds.y);\n maxX = Math.max(maxX, bounds.x2);\n maxY = Math.max(maxY, bounds.y2);\n }\n\n return Number.isFinite(minX) && Number.isFinite(minY) && Number.isFinite(maxX) && Number.isFinite(maxY)\n ? { minX, minY, maxX, maxY }\n : null;\n}\n\n/** Size horizon paper around actual music ink rather than nominal engine height. */\nexport function computeHorizonPaperGeometry(displayList: DisplayList): HorizonPaperGeometry {\n const musicInk = inkBounds(displayList);\n if (!musicInk) {\n return {\n x: -HORIZON_PAPER_PADDING,\n y: -HORIZON_PAPER_PADDING,\n width: displayList.width + HORIZON_PAPER_PADDING * 2,\n height: displayList.height + HORIZON_PAPER_PADDING * 2,\n contentHeight: displayList.height,\n };\n }\n\n const { minX, minY, maxX, maxY } = musicInk;\n const horizontalPadding = Math.min(minX + HORIZON_PAPER_PADDING, displayList.width + HORIZON_PAPER_PADDING - maxX);\n const verticalPadding = Math.max(HORIZON_PAPER_PADDING, horizontalPadding);\n const y = minY - verticalPadding;\n const height = maxY - minY + verticalPadding * 2;\n const paperCenter = y + height / 2;\n return {\n x: -HORIZON_PAPER_PADDING,\n y,\n width: displayList.width + HORIZON_PAPER_PADDING * 2,\n height,\n contentHeight: paperCenter > 0 ? paperCenter * 2 : displayList.height,\n };\n}\n\nfunction detectRecipe(): PaperShadowRecipe {\n if (typeof document === \"undefined\") return LIGHT_RECIPE;\n const theme = document.documentElement.dataset.theme;\n return theme === \"dark\" || theme === \"midnight\" ? DARK_RECIPE : LIGHT_RECIPE;\n}\n\n/**\n * Paint a paper page rectangle at (x, y, w, h) with the canonical paper\n * shadow stack and fill. The fill can be a solid color string or a\n * CanvasPattern (e.g. the noise-textured paper pattern).\n */\nexport function paintPaperPage(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n paperFill: string | CanvasPattern,\n): void {\n const recipe = detectRecipe();\n\n // Cast shadow layers \u2014 paint each as a shadowed fillRect whose footprint\n // is shrunk by |spread|. The fill color doesn't matter for the shadow\n // itself, but we use a neutral so any unblurred edge isn't visible\n // (the real page fill overdraws on top).\n for (const layer of recipe.cast) {\n const s = layer.spread; // typically <= 0\n const fx = x - s;\n const fy = y - s;\n const fw = w + s * 2;\n const fh = h + s * 2;\n if (fw <= 0 || fh <= 0) continue;\n ctx.save();\n ctx.shadowColor = layer.color;\n ctx.shadowBlur = layer.blur;\n ctx.shadowOffsetX = layer.offsetX;\n ctx.shadowOffsetY = layer.offsetY;\n // Fill with a transparent-ish color so the cast color reads cleanly.\n // Using paperFill here would tint the shadow; using black would\n // double-darken. A 1-alpha black on the shrunk footprint is enough\n // for the shadow API to emit pixels without leaving a visible rect.\n ctx.fillStyle = \"#000\";\n ctx.fillRect(fx, fy, fw, fh);\n ctx.restore();\n }\n\n // Real page fill \u2014 no shadow, overdraws the shadow-casting rects above.\n ctx.fillStyle = paperFill;\n ctx.fillRect(x, y, w, h);\n\n // Inset edge stroke (1px just inside the rect).\n ctx.save();\n ctx.strokeStyle = recipe.edgeStrokeColor;\n ctx.lineWidth = 1;\n ctx.strokeRect(x + 0.5, y + 0.5, w - 1, h - 1);\n ctx.restore();\n\n // Lit rim (top 1px highlight).\n ctx.save();\n ctx.fillStyle = recipe.rimColor;\n ctx.fillRect(x, y, w, 1);\n ctx.restore();\n}\n", "/**\n * Playhead Painter \u2014 renders a vertical playhead cursor line during playback.\n *\n * Maps a beat position to an X coordinate using the layout engine's\n * MeasureBounds.beatAnchors, then draws a semi-transparent vertical line\n * spanning all staves in the current system.\n *\n * standard engraving practice draws a similar blue playhead line spanning\n * all staves in the system during playback (ScoreView::setPlayPos).\n */\n\nimport type { MeasureBounds, DisplayList } from \"./wasm\";\nimport type { StaffInfo } from \"./overlayPainter\";\n\n/** A playhead position expressed as a measure index and beat within that measure. */\nexport interface PlayheadPosition {\n /** 0-based measure index. */\n measureIndex: number;\n /** Beat position within the measure (0 = start of measure). */\n beat: number;\n}\n\n/**\n * Where the playhead was drawn, in the painter's coordinate space. Returned by\n * `paintPlayheadAtPosition` so callers can drive auto-scroll: `x` positions the\n * view horizontally, and the `yTop`/`yBottom` band lets a tall system be\n * scrolled fully into view vertically.\n */\nexport interface PlayheadDraw {\n x: number;\n yTop: number;\n yBottom: number;\n}\n\n/** Default playhead color: semi-transparent blue, matching standard practice. */\nconst PLAYHEAD_COLOR = \"rgba(33, 150, 243, 0.6)\";\n/** Default playhead line width in score-space pixels. */\nconst PLAYHEAD_WIDTH = 2;\n\n/**\n * Map a (measureIndex, beat) position to an X coordinate in score space\n * using the layout engine's beat anchors from MeasureBounds.\n *\n * Uses linear interpolation between adjacent beat anchors.\n * Returns null if the measure or beat position cannot be resolved.\n *\n * Barline smoothing: a measure's final beat anchor is the synthetic\n * `(totalBeats, rightEdge)` pair, but the next measure's beat 0 sits a\n * `prefixWidth` further right (trailing pad + barline + next prefix). Left\n * as-is, the playhead snaps across that dead zone the instant `beat` resets\n * at the boundary. When the next measure is on the same system we instead map\n * `totalBeats` to the next measure's first-content X, so the cursor glides\n * continuously across the barline over the final note's duration.\n */\nexport function beatToX(position: PlayheadPosition, measureBounds: MeasureBounds[]): number | null {\n // Find the bounds for this measure (any part index \u2014 they share X positions)\n const bounds =\n measureBounds.find((b) => b.index === position.measureIndex && b.partIndex === 0) ??\n measureBounds.find((b) => b.index === position.measureIndex);\n if (!bounds) return null;\n const endX = nextMeasureContinuityX(bounds, position.measureIndex, measureBounds);\n return interpolateBeatAnchors(bounds, position.beat, endX);\n}\n\n/**\n * The X the playhead should reach at this measure's end (`totalBeats`) so the\n * barline crossing is continuous \u2014 the next measure's first-content X, or\n * `undefined` when smoothing shouldn't apply (no next measure, or it starts a\n * new system / page so the cursor must reach this line's right edge first).\n */\nfunction nextMeasureContinuityX(\n bounds: MeasureBounds,\n measureIndex: number,\n measureBounds: MeasureBounds[],\n): number | undefined {\n const next =\n measureBounds.find((b) => b.index === measureIndex + 1 && b.partIndex === 0) ??\n measureBounds.find((b) => b.index === measureIndex + 1);\n if (!next) return undefined;\n\n // Only smooth across the barline within a system. Across a system/page break\n // the next measure is on another line, so the playhead should still finish at\n // this measure's right edge and then jump to the next line's start.\n if (bounds.systemIndex !== undefined && next.systemIndex !== undefined && next.systemIndex !== bounds.systemIndex) {\n return undefined;\n }\n\n // First-content X of the next measure: its beat-0 anchor, else its prefix end.\n const nextStart =\n next.beatAnchors && next.beatAnchors.length > 0 ? next.beatAnchors[0]![1] : next.x + next.prefixWidth;\n\n // Guard: only ever smooth forward (the next measure should be to the right).\n return nextStart > bounds.x + bounds.width ? nextStart : undefined;\n}\n\n/**\n * Interpolate beat position to X coordinate using a measure's beat anchors.\n *\n * `endX`, when provided, overrides the X of the final anchor *only when that\n * anchor is the synthetic measure-end anchor* (its beat \u2248 `totalBeats`). This\n * is how barline smoothing maps a measure's end to the next measure's start\n * without disturbing real note anchors.\n */\nfunction interpolateBeatAnchors(bounds: MeasureBounds, beat: number, endX?: number): number | null {\n const anchors = bounds.beatAnchors;\n if (!anchors || anchors.length === 0) {\n // No anchors \u2014 use linear interpolation across measure width\n if (bounds.totalBeats <= 0) return bounds.x + bounds.prefixWidth;\n const contentWidth = bounds.width - bounds.prefixWidth;\n const fraction = beat / bounds.totalBeats;\n return bounds.x + bounds.prefixWidth + fraction * contentWidth;\n }\n\n // Clamp beat to valid range\n const clampedBeat = Math.max(0, beat);\n\n // Effective X of the final anchor. The layout always appends a synthetic\n // (totalBeats, rightEdge) anchor; when `endX` is supplied and the last anchor\n // is that synthetic end (beat \u2248 totalBeats), use it so the barline crossing\n // is continuous. Real note anchors are never overridden.\n const lastIdx = anchors.length - 1;\n const lastIsMeasureEnd = Math.abs(anchors[lastIdx]![0] - bounds.totalBeats) < 1e-6;\n const lastX = endX !== undefined && lastIsMeasureEnd ? endX : anchors[lastIdx]![1];\n const xAt = (i: number): number => (i === lastIdx ? lastX : anchors[i]![1]);\n\n // Before first anchor: extrapolate left\n const first = anchors[0]!;\n if (clampedBeat <= first[0]) {\n if (anchors.length === 1) return xAt(0);\n const second = anchors[1]!;\n const pxPerBeat = second[0] !== first[0] ? (xAt(1) - first[1]) / (second[0] - first[0]) : 0;\n return first[1] + (clampedBeat - first[0]) * pxPerBeat;\n }\n\n // After last anchor: extrapolate right\n const last = anchors[lastIdx]!;\n if (clampedBeat >= last[0]) {\n if (anchors.length === 1) return lastX;\n const prev = anchors[lastIdx - 1]!;\n const pxPerBeat = last[0] !== prev[0] ? (lastX - xAt(lastIdx - 1)) / (last[0] - prev[0]) : 0;\n return lastX + (clampedBeat - last[0]) * pxPerBeat;\n }\n\n // Between anchors: linear interpolation\n for (let i = 0; i < lastIdx; i++) {\n const lo = anchors[i]!;\n const hi = anchors[i + 1]!;\n if (clampedBeat >= lo[0] && clampedBeat <= hi[0]) {\n const range = hi[0] - lo[0];\n if (range === 0) return xAt(i);\n const t = (clampedBeat - lo[0]) / range;\n return xAt(i) + t * (xAt(i + 1) - xAt(i));\n }\n }\n\n return lastX;\n}\n\n/**\n * Find the Y extent (top and bottom) of all staves that contain a given X\n * position. Groups staves into the \"system\" at that X coordinate.\n *\n * Returns null if no staves are found at the given X.\n *\n * `pageYRange` defines the vertical bounds of the page that contains the\n * cursor's measure. The upper bound (`yEnd`) should be the *next* page's\n * `yOffset` (or `+Infinity` for the last page) rather than `yOffset + height`,\n * so that systems which overflow the fixed page height are still attributed to\n * the page that contains their first stave.\n */\nexport function findSystemYExtent(\n staves: StaffInfo[],\n x: number,\n pageYRange?: { yOffset: number; yEnd: number },\n systemYRange?: { yTop: number; yBottom: number },\n): { yTop: number; yBottom: number } | null {\n // Collect staves that span this X position (with margin)\n let matching = staves.filter((s) => x >= s.x - s.spatium && x <= s.xEnd + s.spatium);\n\n // Restrict to the system's vertical band so only staves belonging to the\n // cursor's system are included \u2014 otherwise, in a part view with one staff\n // per system, every system below/above the cursor matches the X filter and\n // the playhead stretches the full page.\n //\n // `systemYRange` is derived from the cursor system's `systemIndex`, which is\n // globally unique, so it isolates the system precisely AND is immune to page\n // overflow. When it is available it fully supersedes the page-based filter\n // below: a tall orchestral system can extend past the next page's `yOffset`,\n // and applying `pageYRange` first would wrongly prune the overflowing lower\n // staves (Trombones \u2192 Piano) before this band could include them.\n if (systemYRange) {\n // Allow a small tolerance (half a spatium) on either side so staves\n // whose Y line up with the band's edge are not excluded by fp jitter.\n const tol = (matching[0]?.spatium ?? 0) * 0.5;\n matching = matching.filter((s) => s.y + s.height >= systemYRange.yTop - tol && s.y <= systemYRange.yBottom + tol);\n } else if (pageYRange) {\n // Fallback when no system band is known: filter to staves on the cursor's\n // page only. Without this, staves on different pages that share the same X\n // range would all match, causing the playhead to span the entire score.\n matching = matching.filter((s) => s.y >= pageYRange.yOffset && s.y < pageYRange.yEnd);\n }\n\n if (matching.length === 0) return null;\n\n const margin = matching[0]!.spatium * 0.5;\n const yTop = Math.min(...matching.map((s) => s.y)) - margin;\n const yBottom = Math.max(...matching.map((s) => s.y + s.height)) + margin;\n\n return { yTop, yBottom };\n}\n\n/**\n * Paint a playhead cursor line on a canvas context.\n * Coordinates are in score space \u2014 the caller must apply viewport transform first.\n *\n * @param ctx Canvas 2D context with viewport transform already applied\n * @param x X coordinate in score space\n * @param yTop Top Y of the line in score space\n * @param yBottom Bottom Y of the line in score space\n * @param color Optional override color (default: semi-transparent blue)\n * @param lineWidth Optional override width (default: 2px)\n */\nexport function paintPlayhead(\n ctx: CanvasRenderingContext2D,\n x: number,\n yTop: number,\n yBottom: number,\n color: string = PLAYHEAD_COLOR,\n lineWidth: number = PLAYHEAD_WIDTH,\n): void {\n ctx.save();\n ctx.strokeStyle = color;\n ctx.lineWidth = lineWidth;\n ctx.lineCap = \"butt\";\n ctx.beginPath();\n ctx.moveTo(x, yTop);\n ctx.lineTo(x, yBottom);\n ctx.stroke();\n ctx.restore();\n}\n\n/**\n * High-level function: given a playhead position, display list, and detected\n * staves, paint the playhead cursor on the canvas.\n *\n * Returns the X coordinate of the playhead (for auto-scroll), or null if\n * the position could not be resolved.\n */\n/**\n * Find which page (if any) contains a given Y coordinate.\n */\nexport function findPageForY(\n pages: Array<{ yOffset: number; height: number }>,\n y: number,\n): { yOffset: number; height: number; index: number } | null {\n for (let i = 0; i < pages.length; i++) {\n const p = pages[i]!;\n if (y >= p.yOffset && y < p.yOffset + p.height) {\n return { yOffset: p.yOffset, height: p.height, index: i };\n }\n }\n return null;\n}\n\nexport function paintPlayheadAtPosition(\n ctx: CanvasRenderingContext2D,\n position: PlayheadPosition,\n displayList: DisplayList,\n staves: StaffInfo[],\n color?: string,\n): PlayheadDraw | null {\n if (!displayList.measureBounds || displayList.measureBounds.length === 0) {\n return null;\n }\n\n const x = beatToX(position, displayList.measureBounds);\n if (x === null) return null;\n\n // Determine the page for this measure so we only match staves on that page.\n // Use the *next* page's yOffset (or +Infinity) as the upper bound rather\n // than the current page's height, so that systems which overflow the fixed\n // page height still attribute their lower staves to the correct page.\n const pages = displayList.pages;\n let pageYRange: { yOffset: number; yEnd: number } | undefined;\n const cursorBounds =\n displayList.measureBounds.find((b) => b.index === position.measureIndex && b.partIndex === 0) ??\n displayList.measureBounds.find((b) => b.index === position.measureIndex);\n if (pages && pages.length > 0 && cursorBounds) {\n const page = findPageForY(pages, cursorBounds.y);\n if (page) {\n const next = pages[page.index + 1];\n pageYRange = {\n yOffset: page.yOffset,\n yEnd: next ? next.yOffset : Number.POSITIVE_INFINITY,\n };\n }\n }\n\n // Further restrict to the system that contains the cursor's measure.\n // Without this, every system in a single-instrument part view matches the\n // X filter (same column across systems) and the playhead spans the whole\n // page. We narrow to staves whose Y lies within the system's vertical band,\n // computed from all measure bounds sharing the cursor's `systemIndex`.\n let systemYRange: { yTop: number; yBottom: number } | undefined;\n if (cursorBounds && cursorBounds.systemIndex !== undefined) {\n const sysIdx = cursorBounds.systemIndex;\n let yTop = Number.POSITIVE_INFINITY;\n let yBottom = Number.NEGATIVE_INFINITY;\n for (const b of displayList.measureBounds) {\n if (b.systemIndex === sysIdx) {\n if (b.y < yTop) yTop = b.y;\n if (b.y + b.height > yBottom) yBottom = b.y + b.height;\n }\n }\n if (Number.isFinite(yTop) && Number.isFinite(yBottom)) {\n systemYRange = { yTop, yBottom };\n }\n }\n\n const extent = findSystemYExtent(staves, x, pageYRange, systemYRange);\n if (extent) {\n paintPlayhead(ctx, x, extent.yTop, extent.yBottom, color);\n return { x, yTop: extent.yTop, yBottom: extent.yBottom };\n }\n // Fallback: draw across entire display list height\n paintPlayhead(ctx, x, 0, displayList.height, color);\n return { x, yTop: 0, yBottom: displayList.height };\n}\n", "/**\n * Build identity and asset locations.\n *\n * The prebuilt distribution defines `__SCORE_ENGINE_BUNDLE__` (plus version\n * and commit) so the engine finds `wasm/`, `fonts/` and its worker next to the\n * bundle file. Inside Viritura's own apps these are undefined and the host's\n * asset base (Vite `BASE_URL` / `<base>`) applies.\n */\n\ndeclare const __SCORE_ENGINE_BUNDLE__: boolean | undefined;\ndeclare const __SCORE_ENGINE_COMMIT__: string | undefined;\n\n/** Keep in sync with package.json (asserted by tests). */\nexport const PACKAGE_VERSION = \"0.1.1-next.4\";\n\nconst BUNDLE_WORKER_FILE = \"score-engine.worker.js\";\n\nexport function buildCommit(): string | null {\n return typeof __SCORE_ENGINE_COMMIT__ === \"string\" && __SCORE_ENGINE_COMMIT__ ? __SCORE_ENGINE_COMMIT__ : null;\n}\n\n/** Asset base shipped alongside the prebuilt bundle, if any. */\nexport function bundledAssetBase(): string | undefined {\n if (typeof __SCORE_ENGINE_BUNDLE__ === \"undefined\" || !__SCORE_ENGINE_BUNDLE__) return undefined;\n // Held in a variable so app bundlers don't treat the directory as an asset import.\n const moduleUrl = import.meta.url;\n return new URL(\"./\", moduleUrl).href;\n}\n\n/**\n * Start the engine's layout worker from its default location. Prebuilt\n * bundles ship the worker beside `wasm/` and `fonts/`, so it follows the\n * asset base when a host serves those files from somewhere else.\n */\nexport function spawnDefaultLayoutWorker(assetBaseUrl: string): Worker {\n // Checked inline (not via a helper) so the bundle build folds the flag and\n // drops the Vite-only branch below, which app bundlers would try to resolve.\n if (typeof __SCORE_ENGINE_BUNDLE__ !== \"undefined\" && __SCORE_ENGINE_BUNDLE__) {\n return new Worker(new URL(BUNDLE_WORKER_FILE, assetBaseUrl), { type: \"module\" });\n }\n return new Worker(new URL(\"./layoutWorker.ts\", import.meta.url), { type: \"module\" });\n}\n", "/**\n * Opaque public display-list handles.\n *\n * Consumers only ever see `DisplayList` handles; the renderer's full display\n * list stays behind a WeakMap so its shape is not part of the public API.\n */\n\nimport type { DisplayList as RendererDisplayList, PageLayout } from \"@viritura/renderer\";\nimport type { DisplayList } from \"./types\";\n\nconst internals = new WeakMap<DisplayList, RendererDisplayList>();\n\n/** Page layouts of a renderer display list; unpaged layouts form one page. */\nexport function pageLayoutsOf(displayList: RendererDisplayList): readonly PageLayout[] {\n if (displayList.pages?.length) return displayList.pages;\n return [{ pageNumber: 1, systemIndices: [], yOffset: 0, height: displayList.height }];\n}\n\n/** Wrap a renderer display list in a frozen public handle. */\nexport function wrapDisplayList(displayList: RendererDisplayList): DisplayList {\n const handle = Object.freeze({\n width: displayList.width,\n height: displayList.height,\n pageCount: pageLayoutsOf(displayList).length,\n paged: Boolean(displayList.pages?.length),\n }) as unknown as DisplayList;\n internals.set(handle, displayList);\n return handle;\n}\n\n/** Resolve a public handle back to the renderer display list. */\nexport function unwrapDisplayList(handle: DisplayList): RendererDisplayList {\n const displayList = internals.get(handle);\n if (!displayList) {\n throw new TypeError(\"DisplayList was not produced by this score engine.\");\n }\n return displayList;\n}\n", "/**\n * Typed errors for @viritura/score-engine.\n *\n * Public consumers can `instanceof`-check these to differentiate failure\n * modes without parsing error message strings.\n */\n\n/** Engine asset (WASM, font) failed to load. */\nexport class EngineLoadError extends Error {\n override readonly name = \"EngineLoadError\";\n constructor(\n message: string,\n /** What failed to load: `wasm`, `font`, or `unknown`. */\n public readonly code: \"wasm\" | \"font\" | \"unknown\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n\n/** Input MNX could not be parsed (malformed JSON or invalid schema). */\nexport class ParseError extends Error {\n override readonly name = \"ParseError\";\n constructor(\n message: string,\n /** Reason: `json` (syntax), `schema` (shape), or `unsupported` (unknown feature). */\n public readonly code: \"json\" | \"schema\" | \"unsupported\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n\n/** Layout engine failed (typically WASM panic or out-of-memory). */\nexport class LayoutError extends Error {\n override readonly name = \"LayoutError\";\n constructor(\n message: string,\n /** Reason: `wasm` (engine panic), `oom`, or `unknown`. */\n public readonly code: \"wasm\" | \"oom\" | \"unknown\",\n public readonly cause?: unknown,\n ) {\n super(message);\n }\n}\n", "/**\n * Geometry queries over a computed layout: pages, systems, measures, and the\n * mapping between musical positions and page-local canvas coordinates.\n */\n\nimport {\n beatToX,\n computeHorizonPaperGeometry,\n type DisplayList as RendererDisplayList,\n type MeasureBounds,\n type PageLayout,\n} from \"@viritura/renderer\";\nimport { pageLayoutsOf } from \"./displayListHandle\";\nimport type {\n CanvasBeatHit,\n CanvasBeatPosition,\n HorizonPaper,\n MeasureGeometry,\n PageGeometry,\n PartInfo,\n PlayheadGeometry,\n ScoreMeasurements,\n ScorePosition,\n SystemGeometry,\n} from \"./types\";\n\nfunction boundsPartId(bounds: MeasureBounds): string {\n return bounds.partId || `#${bounds.partIndex}`;\n}\n\n/** Parts in the layout, in score order. */\nfunction layoutParts(displayList: RendererDisplayList): PartInfo[] {\n if (displayList.parts?.length) {\n return displayList.parts.map((part) => ({ id: part.id, index: part.index, name: part.name }));\n }\n const seen = new Map<string, PartInfo>();\n for (const bounds of displayList.measureBounds ?? []) {\n const id = boundsPartId(bounds);\n if (!seen.has(id)) seen.set(id, { id, index: bounds.partIndex, name: \"\" });\n }\n return [...seen.values()].sort((a, b) => a.index - b.index);\n}\n\nfunction pageIndexForY(pages: readonly PageLayout[], y: number): number {\n for (let i = 0; i < pages.length; i++) {\n const page = pages[i]!;\n if (y >= page.yOffset && y < page.yOffset + page.height) return i;\n }\n return Math.max(0, pages.length - 1);\n}\n\nexport function measureLayout(displayList: RendererDisplayList): ScoreMeasurements {\n const pages: PageGeometry[] = pageLayoutsOf(displayList).map((page, index) => ({\n index,\n width: displayList.width,\n height: page.height,\n offsetY: page.yOffset,\n }));\n return {\n pageCount: pages.length,\n pages,\n totalHeight: pages.reduce((sum, page) => sum + page.height, 0),\n maxPageWidth: displayList.width,\n parts: layoutParts(displayList),\n };\n}\n\nfunction visibleBounds(displayList: RendererDisplayList): MeasureBounds[] {\n return (displayList.measureBounds ?? []).filter((bounds) => !bounds.ghostStaff && !bounds.isExpansion);\n}\n\nexport function measureGeometry(displayList: RendererDisplayList): MeasureGeometry[] {\n const pages = pageLayoutsOf(displayList);\n return visibleBounds(displayList).map((bounds) => {\n const page = pageIndexForY(pages, bounds.y);\n return {\n index: bounds.index,\n ...(bounds.measureId ? { measureId: bounds.measureId } : {}),\n partId: boundsPartId(bounds),\n staffIndex: bounds.staffIndex,\n systemIndex: bounds.systemIndex ?? 0,\n page,\n x: bounds.x,\n y: bounds.y - pages[page]!.yOffset,\n width: bounds.width,\n height: bounds.height,\n contentX: bounds.x + bounds.prefixWidth,\n totalBeats: bounds.totalBeats,\n };\n });\n}\n\nexport function systemGeometry(displayList: RendererDisplayList): SystemGeometry[] {\n const extents = new Map<number, { page: number; x1: number; y1: number; x2: number; y2: number }>();\n for (const measure of measureGeometry(displayList)) {\n const current = extents.get(measure.systemIndex);\n const x2 = measure.x + measure.width;\n const y2 = measure.y + measure.height;\n if (!current) {\n extents.set(measure.systemIndex, { page: measure.page, x1: measure.x, y1: measure.y, x2, y2 });\n continue;\n }\n current.x1 = Math.min(current.x1, measure.x);\n current.y1 = Math.min(current.y1, measure.y);\n current.x2 = Math.max(current.x2, x2);\n current.y2 = Math.max(current.y2, y2);\n }\n return [...extents.entries()]\n .sort(([a], [b]) => a - b)\n .map(([index, e]) => ({ index, page: e.page, x: e.x1, y: e.y1, width: e.x2 - e.x1, height: e.y2 - e.y1 }));\n}\n\nexport function horizonPaper(displayList: RendererDisplayList): HorizonPaper {\n const paper = computeHorizonPaperGeometry(displayList);\n return { x: paper.x, y: paper.y, width: paper.width, height: paper.height, contentHeight: paper.contentHeight };\n}\n\n/** First (top) staff of each measure for one part, in measure order. */\nfunction partMeasures(displayList: RendererDisplayList, partId: string | undefined): MeasureBounds[] {\n const bounds = visibleBounds(displayList);\n const target = partId ?? (bounds[0] ? boundsPartId(bounds[0]) : undefined);\n if (target == null) return [];\n const byMeasure = new Map<number, MeasureBounds>();\n for (const b of bounds) {\n if (boundsPartId(b) !== target) continue;\n const existing = byMeasure.get(b.index);\n if (!existing || b.staffIndex < existing.staffIndex) byMeasure.set(b.index, b);\n }\n return [...byMeasure.values()].sort((a, b) => a.index - b.index);\n}\n\nfunction xAtBeat(measures: MeasureBounds[], bounds: MeasureBounds, beatInMeasure: number): number {\n return beatToX({ measureIndex: bounds.index, beat: beatInMeasure }, measures) ?? bounds.x + bounds.prefixWidth;\n}\n\nfunction resolvePosition(\n displayList: RendererDisplayList,\n position: ScorePosition,\n): { bounds: MeasureBounds; x: number } | null {\n const measures = partMeasures(displayList, position.partId);\n if (\"measureIndex\" in position) {\n const bounds = measures.find((m) => m.index === position.measureIndex);\n return bounds ? { bounds, x: xAtBeat(measures, bounds, position.beat) } : null;\n }\n let start = 0;\n for (const bounds of measures) {\n if (position.beat < start + bounds.totalBeats) {\n return { bounds, x: xAtBeat(measures, bounds, Math.max(0, position.beat - start)) };\n }\n start += bounds.totalBeats;\n }\n return null;\n}\n\nexport function positionToCanvas(displayList: RendererDisplayList, position: ScorePosition): CanvasBeatPosition | null {\n const resolved = resolvePosition(displayList, position);\n if (!resolved) return null;\n const pages = pageLayoutsOf(displayList);\n const page = pageIndexForY(pages, resolved.bounds.y);\n return { page, x: resolved.x, y: resolved.bounds.y - pages[page]!.yOffset, height: resolved.bounds.height };\n}\n\nexport function playheadAt(displayList: RendererDisplayList, position: ScorePosition): PlayheadGeometry | null {\n const resolved = resolvePosition(displayList, position);\n if (!resolved) return null;\n const systemIndex = resolved.bounds.systemIndex ?? 0;\n let top = Number.POSITIVE_INFINITY;\n let bottom = Number.NEGATIVE_INFINITY;\n for (const bounds of visibleBounds(displayList)) {\n if ((bounds.systemIndex ?? 0) !== systemIndex) continue;\n top = Math.min(top, bounds.y);\n bottom = Math.max(bottom, bounds.y + bounds.height);\n }\n const pages = pageLayoutsOf(displayList);\n const page = pageIndexForY(pages, resolved.bounds.y);\n return { page, x: resolved.x, y: top - pages[page]!.yOffset, height: bottom - top, systemIndex };\n}\n\nfunction beatInMeasureAtX(bounds: MeasureBounds, x: number): number {\n const anchors = bounds.beatAnchors;\n if (anchors && anchors.length >= 2) {\n if (x <= anchors[0]![1]) return anchors[0]![0];\n for (let i = 0; i < anchors.length - 1; i++) {\n const [b0, x0] = anchors[i]!;\n const [b1, x1] = anchors[i + 1]!;\n if (x >= x0 && x <= x1) return b0 + (x1 > x0 ? (x - x0) / (x1 - x0) : 0) * (b1 - b0);\n }\n return anchors[anchors.length - 1]![0];\n }\n const contentWidth = bounds.width - bounds.prefixWidth;\n if (bounds.totalBeats <= 0 || contentWidth <= 0) return 0;\n return (Math.max(0, x - bounds.x - bounds.prefixWidth) / contentWidth) * bounds.totalBeats;\n}\n\nexport function canvasToBeat(\n displayList: RendererDisplayList,\n page: number,\n x: number,\n y: number,\n): CanvasBeatHit | null {\n const pages = pageLayoutsOf(displayList);\n const absY = y + (pages[page]?.yOffset ?? 0);\n const hit = visibleBounds(displayList).find(\n (b) => x >= b.x && x <= b.x + b.width && absY >= b.y && absY <= b.y + b.height,\n );\n if (!hit) return null;\n const partId = boundsPartId(hit);\n const prior = partMeasures(displayList, partId)\n .filter((b) => b.index < hit.index)\n .reduce((sum, b) => sum + b.totalBeats, 0);\n return { beat: prior + beatInMeasureAtX(hit, x), measureIndex: hit.index, partId };\n}\n", "/** Translation of public layout options into engine calls, shared by main thread and worker. */\n\nimport { LayoutError, ParseError } from \"./errors\";\nimport type { LayoutOptions } from \"./types\";\n\nexport interface LayoutArgs {\n readonly json: string;\n readonly spatium: number;\n readonly pageWidth: number;\n readonly scoreIndex: number;\n readonly pageSetupJson: string | undefined;\n}\n\nexport function mnxJson(mnx: string | object): string {\n if (typeof mnx === \"string\") return mnx;\n try {\n return JSON.stringify(mnx);\n } catch (err) {\n throw new ParseError(`MNX input is not JSON-serializable: ${messageOf(err)}`, \"json\", err);\n }\n}\n\nexport function layoutArgs(mnx: string | object, opts: LayoutOptions): LayoutArgs {\n const spatium = opts.spatium ?? 7;\n if (!(spatium > 0)) throw new RangeError(`spatium must be positive, got ${spatium}`);\n const setup = opts.pageSetup;\n return {\n json: mnxJson(mnx),\n spatium,\n pageWidth: opts.pageWidth,\n scoreIndex: opts.scoreIndex ?? 0,\n // The engine takes page geometry in staff spaces.\n pageSetupJson: setup\n ? JSON.stringify({\n page_height: setup.height / spatium,\n page_margin_top: setup.margins.top / spatium,\n page_margin_right: setup.margins.right / spatium,\n page_margin_bottom: setup.margins.bottom / spatium,\n page_margin_left: setup.margins.left / spatium,\n })\n : undefined,\n };\n}\n\nexport function messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/** Classify a raw engine failure. The Rust engine reports errors as strings. */\nexport function layoutFailure(err: unknown): ParseError | LayoutError {\n if (err instanceof ParseError || err instanceof LayoutError) return err;\n const msg = messageOf(err);\n if (/parse|json|schema|missing field|expected|invalid type|unknown variant/i.test(msg)) {\n return new ParseError(`Failed to parse MNX: ${msg}`, \"schema\", err);\n }\n if (/out of memory|memory access|allocation/i.test(msg)) {\n return new LayoutError(`Layout failed: ${msg}`, \"oom\", err);\n }\n return new LayoutError(`Layout failed: ${msg}`, \"wasm\", err);\n}\n", "import { decodeBinaryDisplayList } from \"@viritura/renderer\";\nimport { spawnDefaultLayoutWorker } from \"./buildInfo\";\nimport { wrapDisplayList } from \"./displayListHandle\";\nimport { EngineLoadError, LayoutError } from \"./errors\";\nimport { layoutArgs, layoutFailure } from \"./layoutRequest\";\nimport type { LayoutWorkerCommand, LayoutWorkerRequest, LayoutWorkerResponse } from \"./layoutWorkerProtocol\";\nimport type { DisplayList, LayoutOptions, LayoutWorker, LayoutWorkerOptions } from \"./types\";\n\ninterface Pending {\n resolve(data: Float32Array | undefined): void;\n reject(err: Error): void;\n}\n\nexport function createLayoutWorker(assetBaseUrl: string, opts: LayoutWorkerOptions = {}): LayoutWorker {\n const worker = opts.url ? new Worker(opts.url, { type: \"module\" }) : spawnDefaultLayoutWorker(assetBaseUrl);\n const pending = new Map<number, Pending>();\n let nextId = 0;\n let disposed = false;\n\n const failAll = (err: Error): void => {\n for (const p of pending.values()) p.reject(err);\n pending.clear();\n };\n\n worker.onmessage = (event: MessageEvent<LayoutWorkerResponse>) => {\n const res = event.data;\n const p = pending.get(res.id);\n if (!p) return;\n pending.delete(res.id);\n if (res.ok) p.resolve(res.data);\n else if (res.kind === \"load\") p.reject(new EngineLoadError(`Layout worker failed to load: ${res.message}`, \"wasm\"));\n else p.reject(layoutFailure(new Error(res.message)));\n };\n worker.onerror = (event: ErrorEvent) => {\n event.preventDefault();\n failAll(new EngineLoadError(`Layout worker error: ${event.message || \"script failed to load\"}`, \"wasm\", event));\n };\n\n const send = (command: LayoutWorkerCommand): Promise<Float32Array | undefined> => {\n if (disposed) return Promise.reject(new LayoutError(\"Layout worker has been disposed\", \"unknown\"));\n const id = nextId++;\n return new Promise((resolve, reject) => {\n pending.set(id, { resolve, reject });\n const request: LayoutWorkerRequest = { ...command, id };\n worker.postMessage(request);\n });\n };\n\n const ready = send({ type: \"init\", assetBaseUrl });\n // Surface init failures through layout() rather than as unhandled rejections.\n ready.catch(() => undefined);\n\n return {\n async layout(mnx: string | object, layoutOpts: LayoutOptions): Promise<DisplayList> {\n const args = layoutArgs(mnx, layoutOpts);\n await ready;\n const data = await send({ type: \"layout\", args });\n if (!data) throw new LayoutError(\"Layout worker returned no data\", \"unknown\");\n return wrapDisplayList(decodeBinaryDisplayList(data));\n },\n dispose(): void {\n if (disposed) return;\n disposed = true;\n worker.terminate();\n failAll(new LayoutError(\"Layout worker has been disposed\", \"unknown\"));\n },\n };\n}\n", "/**\n * Ink-only Canvas painting for public display lists.\n *\n * `paint` never clears or scales the canvas: callers own the context\n * transform (device-pixel ratio, zoom, scroll) and any page presentation.\n */\n\nimport {\n paintCommandsCulled,\n splitCommandsByPage,\n type DisplayList as RendererDisplayList,\n type PageLayout,\n type RenderCommand,\n} from \"@viritura/renderer\";\nimport type { PaintOptions } from \"./types\";\n\ninterface PageSlice {\n readonly commands: RenderCommand[];\n readonly layout: PageLayout;\n}\n\nconst pageSlices = new WeakMap<RendererDisplayList, PageSlice[]>();\nconst recoloured = new WeakMap<readonly RenderCommand[], Map<string, RenderCommand[]>>();\n\nconst DEFAULT_INK = /^#0{3}(?:0{3})?$/i;\n\n/** Commands of one page, split once per display list. */\nfunction pageSlice(displayList: RendererDisplayList, page: number): PageSlice | undefined {\n let slices = pageSlices.get(displayList);\n if (!slices) {\n slices = splitCommandsByPage(displayList);\n pageSlices.set(displayList, slices);\n }\n return slices[page];\n}\n\n/** Substitute `ink` for default black ink, keeping explicitly coloured elements. */\nexport function withInk(commands: RenderCommand[], ink: string | undefined): RenderCommand[] {\n if (!ink) return commands;\n let byInk = recoloured.get(commands);\n if (!byInk) {\n byInk = new Map();\n recoloured.set(commands, byInk);\n }\n const cached = byInk.get(ink);\n if (cached) return cached;\n const result = commands.map((cmd) =>\n \"color\" in cmd && typeof cmd.color === \"string\" && DEFAULT_INK.test(cmd.color) ? { ...cmd, color: ink } : cmd,\n ) as RenderCommand[];\n byInk.set(ink, result);\n return result;\n}\n\n/** Recolour a whole display list (used for SVG export). */\nexport function displayListWithInk(displayList: RendererDisplayList, ink: string | undefined): RendererDisplayList {\n if (!ink) return displayList;\n return { ...displayList, commands: withInk(displayList.commands, ink) };\n}\n\nexport function paintInk(ctx: CanvasRenderingContext2D, displayList: RendererDisplayList, opts: PaintOptions): void {\n const slice = pageSlice(displayList, opts.page ?? 0);\n if (!slice) return;\n const width = displayList.width;\n const height = slice.layout.height;\n const region = opts.region ?? { x: 0, y: 0, width, height };\n ctx.save();\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (opts.background) {\n ctx.fillStyle = opts.background;\n ctx.fillRect(region.x, region.y, region.width, region.height);\n }\n // Commands are in absolute (stacked-page) coordinates; shift the page to\n // the origin so geometry and painting agree on page-local space.\n const offsetY = slice.layout.yOffset;\n ctx.translate(0, -offsetY);\n paintCommandsCulled(\n ctx,\n withInk(slice.commands, opts.ink),\n null,\n region.x,\n region.x + region.width,\n region.y + offsetY,\n region.y + offsetY + region.height,\n );\n ctx.restore();\n}\n", "import { wasmExportSvg, type DisplayList as RendererDisplayList } from \"@viritura/renderer\";\nimport { pageLayoutsOf } from \"./displayListHandle\";\nimport { displayListWithInk } from \"./paint\";\nimport type { SvgOptions } from \"./types\";\n\n/** Display-list units are CSS pixels: 96 per inch. */\nconst MM_PER_UNIT = 25.4 / 96;\n\nconst fontBytes = new Map<string, Promise<Uint8Array>>();\n\nfunction fetchFont(url: string): Promise<Uint8Array> {\n let bytes = fontBytes.get(url);\n if (!bytes) {\n bytes = fetch(url).then(async (res) => {\n if (!res.ok) throw new Error(`Failed to fetch font ${url}: ${res.status}`);\n return new Uint8Array(await res.arrayBuffer());\n });\n // Let a failed fetch be retried.\n bytes.catch(() => fontBytes.delete(url));\n fontBytes.set(url, bytes);\n }\n return bytes;\n}\n\n/**\n * Export one page as standalone SVG. Glyphs and text are converted to path\n * outlines, so the result needs no fonts.\n */\nexport async function exportPageSvg(\n displayList: RendererDisplayList,\n assetBase: string,\n opts: SvgOptions,\n): Promise<string> {\n const pageIndex = opts.page ?? 0;\n const pages = pageLayoutsOf(displayList);\n const page = pages[pageIndex];\n if (!page) throw new RangeError(`Page ${pageIndex} out of range (0-${pages.length - 1})`);\n const [bravura, text] = await Promise.all([\n fetchFont(`${assetBase}fonts/Bravura.otf`),\n fetchFont(`${assetBase}fonts/LibertinusSerif-Regular.otf`),\n ]);\n const source = displayListWithInk(displayList, opts.ink);\n const svgPages = wasmExportSvg(\n JSON.stringify(source),\n bravura,\n text,\n MM_PER_UNIT,\n 1,\n displayList.width * MM_PER_UNIT,\n page.height * MM_PER_UNIT,\n );\n const svg = svgPages[pageIndex]?.svg;\n if (svg === undefined) throw new RangeError(`Page ${pageIndex} was not exported`);\n return svg;\n}\n", "import { TileCache, type DisplayList as RendererDisplayList } from \"@viritura/renderer\";\nimport { unwrapDisplayList } from \"./displayListHandle\";\nimport { displayListWithInk } from \"./paint\";\nimport type { DisplayList, TilePaintOptions, TileRenderer } from \"./types\";\n\n/** Create a cached tile painter for large unpaged (horizon) layouts. */\nexport function createTileRenderer(): TileRenderer {\n const cache = new TileCache();\n let version = 0;\n let source: RendererDisplayList | null = null;\n let sourceInk: string | undefined;\n let painted: RendererDisplayList | null = null;\n\n return {\n paint(canvas: HTMLCanvasElement, displayList: DisplayList, opts: TilePaintOptions): boolean {\n const raw = unwrapDisplayList(displayList);\n if (raw !== source || opts.ink !== sourceInk || !painted) {\n source = raw;\n sourceInk = opts.ink;\n painted = displayListWithInk(raw, opts.ink);\n version++;\n }\n cache.paintFrame({\n canvas,\n displayList: painted,\n scrollX: opts.scrollX,\n scrollY: opts.scrollY,\n zoom: opts.zoom,\n version,\n glyphAtlas: null,\n viewMode: \"horizon\",\n canvasBg: opts.background ?? \"rgba(0,0,0,0)\",\n paperFill: null,\n });\n return cache.hasPendingTiles;\n },\n invalidate(): void {\n version++;\n cache.invalidate();\n },\n };\n}\n", "import { parseMnxUnvalidated } from \"@viritura/format\";\nimport { generateTimeline } from \"@viritura/midi\";\nimport type { TimedEvent, Timeline, TimelineOptions } from \"./timeline\";\nimport { stablePartIds } from \"./partIds\";\n\n/** Collect only IDs actually supplied by the source (the parser mints missing IDs). */\nfunction authoredEventIds(raw: unknown): Set<string> {\n const ids = new Set<string>();\n function visit(value: unknown): void {\n if (!value || typeof value !== \"object\") return;\n if (Array.isArray(value)) {\n for (const item of value) visit(item);\n return;\n }\n const item = value as Record<string, unknown>;\n if (item[\"type\"] === \"event\" && typeof item[\"id\"] === \"string\" && item[\"id\"]) ids.add(item[\"id\"]);\n for (const child of Object.values(item)) visit(child);\n }\n visit(raw);\n return ids;\n}\n\n/** Build a layout-independent timeline on the MIDI generator's exact written beat axis. */\nexport function buildTimeline(mnx: string | object, opts: TimelineOptions = {}): Timeline {\n const raw: unknown = typeof mnx === \"string\" ? JSON.parse(mnx) : mnx;\n const partIds = stablePartIds(raw);\n const sourceIds = authoredEventIds(raw);\n const midi = generateTimeline(parseMnxUnvalidated(raw), { expandRepeats: opts.repeatExpansion !== \"ignore\" });\n const tempoMap = midi.tempoMap.map((entry) => ({\n beat: (midi.measureStartBeats[entry.measureIndex] ?? 0) + entry.beatInMeasure,\n timeSeconds: entry.timeSeconds,\n bpm: entry.bpm,\n }));\n\n const events: TimedEvent[] = [];\n const open = new Map<string, number[]>();\n for (const ev of midi.events) {\n if (ev.partIndex >= partIds.length || (ev.type !== \"noteOn\" && ev.type !== \"noteOff\")) continue;\n const key = `${ev.partIndex}:${ev.midiNote}:${ev.channel}:${ev.playbackLaneId ?? \"\"}`;\n if (ev.type === \"noteOn\") {\n const beat = ev.scoreBeat ?? midi.model.beatAtTime(ev.time);\n const event: TimedEvent = {\n partId: partIds[ev.partIndex]!,\n beat,\n durationBeats: ev.scoreDurationBeats ?? 0,\n timeSeconds: ev.time,\n midiPitch: ev.midiNote,\n isRest: false,\n };\n if (ev.scoreEventId && sourceIds.has(ev.scoreEventId)) event.eventId = ev.scoreEventId;\n events.push(event);\n const queue = open.get(key) ?? [];\n queue.push(events.length - 1);\n open.set(key, queue);\n } else {\n const index = open.get(key)?.shift();\n if (index !== undefined && events[index]!.durationBeats === 0) {\n events[index]!.durationBeats = midi.model.beatAtTime(ev.time) - events[index]!.beat;\n }\n }\n }\n events.sort((a, b) => a.beat - b.beat);\n return {\n totalBeats: midi.totalBeats,\n totalSeconds: midi.duration,\n partIds,\n events,\n tempoMap,\n };\n}\n", "/**\n * @viritura/format \u2014 MNX parser and serializer.\n *\n * Converts between MNX JSON files and the @viritura/core typed model.\n */\n\nexport { parseMnx, parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./mnx\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./mnx\";\nexport { promote, promoteUnknown, promoteWithDiagnostics } from \"./mnx\";\nexport type { RawScore, PromoteOptions, PromoteResult } from \"./mnx\";\nexport { isRawScore, assertRawScore, validateRawScore, RawScoreValidationFailure } from \"./mnx\";\nexport type { RawScoreValidationError, RawScoreValidationResult } from \"./mnx\";\nexport { recoverInvalidSequences } from \"./mnx\";\nexport type { RecoveredSequence, RecoverInvalidSequencesOptions, SequenceRecoveryResult } from \"./mnx\";\nexport { MNX_SCHEMA_VERSION } from \"./mnx\";\nexport { serializeMnx } from \"./mnx\";\nexport {\n serializeEvent,\n serializeSequenceContent,\n serializeArpeggio,\n serializeNonArpeggio,\n serializeDynamicGroup,\n} from \"./mnx\";\nexport { DeltaSerializer } from \"./mnx\";\nexport type { DeltaSerializationResult } from \"./mnx\";\nexport { loadMnxFromUrl, loadMnxFromString } from \"./mnx\";\nexport { DiagnosticCollector, mnxPointer, mnxPointerChild } from \"./mnx\";\nexport type { MnxDiagnostic, DiagnosticSeverity } from \"./mnx\";\n", "/**\n * Barrel for the MNX subsystem (parser, serializer, validator, loader,\n * promote pipeline, delta serializer, diagnostic plumbing).\n *\n * Consumers should import from `@viritura/format` rather than reaching\n * into this folder directly; the package barrel re-exports through here.\n */\n\nexport { parseMnx, parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./parser\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./parser\";\nexport { promote, promoteUnknown, promoteWithDiagnostics } from \"./promote\";\nexport type { RawScore, PromoteOptions, PromoteResult } from \"./promote\";\nexport { isRawScore, assertRawScore, validateRawScore, RawScoreValidationFailure } from \"./validator\";\nexport type { RawScoreValidationError, RawScoreValidationResult } from \"./validator\";\nexport { recoverInvalidSequences } from \"./sequenceRecovery\";\nexport type { RecoveredSequence, RecoverInvalidSequencesOptions, SequenceRecoveryResult } from \"./sequenceRecovery\";\nexport { MNX_SCHEMA_VERSION } from \"./standaloneValidators\";\nexport { serializeMnx, serializeEvent, serializeSequenceContent } from \"./serializer\";\nexport { serializeArpeggio, serializeNonArpeggio, serializeDynamicGroup } from \"./serializePart\";\nexport { DeltaSerializer } from \"./deltaSerializer\";\nexport type { DeltaSerializationResult } from \"./deltaSerializer\";\nexport { loadMnxFromUrl, loadMnxFromString } from \"./loader\";\nexport { DiagnosticCollector, ptr as mnxPointer, ptrChild as mnxPointerChild } from \"./diagnostics\";\nexport type { MnxDiagnostic, DiagnosticSeverity } from \"./diagnostics\";\n", "/** Validated parser entry point and diagnostics compatibility surface. */\nexport { parseMnx } from \"./validatedParser\";\nexport { parseMnxUnvalidated, parseMnxWithDiagnostics } from \"./parserCore\";\nexport type { ParseMnxOptions, ParseMnxResult } from \"./parserCore\";\n", "/**\n * MNX JSON Parser \u2014 schema-independent model builder.\n * Split into modules: parseGlobal, parseContent, parseParts, parseLayout\n */\n\nimport type {\n Score,\n SoundProfileAssignment,\n Support,\n ScoreMetadata,\n TextStyles,\n ChordSymbolStyle,\n GroupingDisplay,\n TimeSignatureDistribution,\n TimeSignatureGrandStaff,\n TimeSignaturePosition,\n TimeSignatureRenderStyle,\n TimeSignatureSettings,\n TimeSignatureStyles,\n VideoSyncSettings,\n VideoMediaIdentity,\n HitPoint,\n LyricWorkflow,\n LyricWorkflowSource,\n LyricWorkflowToken,\n} from \"@viritura/core\";\nimport { generateId } from \"@viritura/core\";\n\nimport type { SequenceContent } from \"@viritura/core\";\n\ntype Obj = Record<string, unknown>;\n\nimport { parseGlobalMeasures, parseGlobalLyrics, parseGlobalSounds } from \"./parseGlobal\";\nimport { parseParts } from \"./parseParts\";\nimport { parseLayoutDefinition, parseScoreDefinition } from \"./parseLayout\";\nimport type {\n SystemLayout as RawSystemLayout,\n Score as RawScoreDef,\n MeasureGlobal as RawMeasureGlobal,\n LyricsGlobal as RawLyricsGlobal,\n SoundsGlobal as RawSoundsGlobal,\n Part as RawPart,\n} from \"@viritura/core/raw\";\nimport { DiagnosticCollector, ptr, type MnxDiagnostic } from \"./diagnostics\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Main parser entry point\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Options for {@link parseMnxWithDiagnostics}. Pre-alpha; backward-compatible\n * with existing callers via {@link parseMnx} which discards diagnostics.\n */\nexport interface ParseMnxOptions {\n /**\n * When provided, the parser writes lossy-conversion diagnostics here.\n * Inspired by standard practice importer (see\n *\n */\n diagnostics?: DiagnosticCollector;\n /**\n * Optional schema validator. When provided, the parser invokes it on the\n * raw JSON and forwards each issue to {@link diagnostics} as an error with\n * code `\"schema-validation\"`. The validator owns its own engine (e.g.\n * Ajv2020) so this package stays dependency-free.\n *\n * See `apps/website/src/routes/mnx-converter/ValidationPanel.tsx` for an Ajv2020 example.\n */\n validate?: (json: unknown) => ReadonlyArray<{ pointer: string; message: string }>;\n}\n\n/**\n * Result of {@link parseMnxWithDiagnostics}.\n */\nexport interface ParseMnxResult {\n score: Score;\n diagnostics: readonly MnxDiagnostic[];\n}\n\n/**\n * Parse a raw MNX JSON object and collect diagnostics about lossy decisions\n * (unknown fields, fallback substitutions, dropped data).\n */\nexport function parseMnxWithDiagnostics(json: unknown, options: ParseMnxOptions = {}): ParseMnxResult {\n const diagnostics = options.diagnostics ?? new DiagnosticCollector();\n if (options.validate) {\n for (const issue of options.validate(json)) {\n diagnostics.error(issue.pointer, issue.message, \"schema-validation\");\n }\n }\n const score = parseMnxInternal(json, diagnostics);\n return { score, diagnostics: diagnostics.all() };\n}\n\n/** Build the Score model without loading or running the JSON Schema validator. */\nexport function parseMnxUnvalidated(json: unknown): Score {\n return parseMnxInternal(json, new DiagnosticCollector());\n}\n\nfunction parseSupport(supportObj: Obj | undefined): Support | undefined {\n if (!supportObj) return undefined;\n const support: Support = {};\n if (typeof supportObj[\"useAccidentalDisplay\"] === \"boolean\") {\n support.useAccidentalDisplay = supportObj[\"useAccidentalDisplay\"];\n }\n if (typeof supportObj[\"useBeams\"] === \"boolean\") {\n support.useBeams = supportObj[\"useBeams\"];\n }\n return support;\n}\n\nfunction parseRootMetadata(rootX: Obj | undefined): ScoreMetadata | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const m = viritura?.[\"metadata\"] as Obj | undefined;\n if (!m) return undefined;\n const metadata: ScoreMetadata = {};\n if (m[\"title\"]) metadata.title = m[\"title\"] as string;\n if (m[\"subtitle\"]) metadata.subtitle = m[\"subtitle\"] as string;\n if (m[\"composer\"]) metadata.composer = m[\"composer\"] as string;\n if (m[\"lyricist\"]) metadata.lyricist = m[\"lyricist\"] as string;\n if (m[\"arranger\"]) metadata.arranger = m[\"arranger\"] as string;\n if (m[\"copyright\"]) metadata.copyright = m[\"copyright\"] as string;\n return Object.keys(metadata).length > 0 ? metadata : undefined;\n}\n\nfunction parseRootTextStyles(rootX: Obj | undefined): TextStyles | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const ts = viritura?.[\"textStyles\"] as TextStyles | undefined;\n if (!ts || typeof ts !== \"object\" || Object.keys(ts).length === 0) return undefined;\n return ts;\n}\n\nfunction parseLyricWorkflowToken(raw: unknown): LyricWorkflowToken | null {\n if (!raw || typeof raw !== \"object\") return null;\n const value = raw as Obj;\n if (typeof value[\"id\"] !== \"string\" || value[\"id\"].length === 0) return null;\n const token: LyricWorkflowToken = { id: value[\"id\"] };\n if (typeof value[\"text\"] === \"string\") token.text = value[\"text\"];\n if (value[\"type\"] === \"start\" || value[\"type\"] === \"middle\" || value[\"type\"] === \"end\" || value[\"type\"] === \"whole\") {\n token.type = value[\"type\"];\n }\n if (value[\"skip\"] === true) token.skip = true;\n if (typeof value[\"eventId\"] === \"string\" && value[\"eventId\"].length > 0) token.eventId = value[\"eventId\"];\n if (typeof value[\"partId\"] === \"string\" && value[\"partId\"].length > 0) token.partId = value[\"partId\"];\n if (\n typeof value[\"sequenceIndex\"] === \"number\" &&\n Number.isInteger(value[\"sequenceIndex\"]) &&\n value[\"sequenceIndex\"] >= 0\n ) {\n token.sequenceIndex = value[\"sequenceIndex\"];\n }\n return token;\n}\n\nfunction parseRootLyricWorkflow(rootX: Obj | undefined): LyricWorkflow | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"lyricWorkflow\"] as Obj | undefined;\n const rawSources = raw?.[\"sources\"];\n if (!rawSources || typeof rawSources !== \"object\") return undefined;\n const sources: Record<string, LyricWorkflowSource> = {};\n for (const [key, candidate] of Object.entries(rawSources as Obj)) {\n if (!candidate || typeof candidate !== \"object\") continue;\n const value = candidate as Obj;\n if (\n typeof value[\"id\"] !== \"string\" ||\n typeof value[\"lineId\"] !== \"string\" ||\n typeof value[\"text\"] !== \"string\" ||\n !Array.isArray(value[\"tokens\"])\n ) {\n continue;\n }\n const tokens = value[\"tokens\"].map(parseLyricWorkflowToken).filter((token): token is LyricWorkflowToken => !!token);\n const source: LyricWorkflowSource = {\n id: value[\"id\"],\n lineId: value[\"lineId\"],\n text: value[\"text\"],\n tokens,\n };\n if (typeof value[\"language\"] === \"string\" && value[\"language\"].length > 0) source.language = value[\"language\"];\n sources[key] = source;\n }\n return Object.keys(sources).length > 0 ? { sources } : undefined;\n}\n\nconst RENDER_STYLES: readonly TimeSignatureRenderStyle[] = [\n \"standard\",\n \"narrow\",\n \"outsideStaff\",\n \"singleNumber\",\n \"noteValue\",\n];\nconst DISTRIBUTIONS: readonly TimeSignatureDistribution[] = [\"perStaff\", \"perGroup\"];\nconst GRAND_STAFF_OPTIONS: readonly TimeSignatureGrandStaff[] = [\"include\", \"exclude\"];\nconst POSITIONS: readonly TimeSignaturePosition[] = [\"center\", \"top\", \"bottom\", \"above\"];\nconst SENZA_MISURA_DISPLAYS = [\"open\", \"hidden\"] as const;\nconst GROUPING_DISPLAYS: readonly GroupingDisplay[] = [\"standard\", \"additive\", \"annotation\"];\n\nconst LEGACY_TIME_SIGNATURE_SETTINGS: Readonly<Record<string, TimeSignatureSettings>> = {\n normal: {},\n large: { scale: 1.5 },\n narrow: { renderStyle: \"narrow\" },\n aboveStaff: { position: \"above\", scale: 0.8 },\n spanning: { renderStyle: \"outsideStaff\", distribution: \"perGroup\", scale: 2 },\n singleNumber: { renderStyle: \"singleNumber\", scale: 2 },\n noteValue: { renderStyle: \"noteValue\" },\n};\n\nfunction enumValue<T extends string>(values: readonly T[], value: unknown): T | undefined {\n return values.find((candidate) => candidate === value);\n}\n\nfunction parseTimeSignatureSettings(value: unknown): TimeSignatureSettings | undefined {\n if (typeof value === \"string\") return LEGACY_TIME_SIGNATURE_SETTINGS[value];\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Obj;\n const settings: TimeSignatureSettings = {};\n const renderStyle = enumValue(RENDER_STYLES, raw[\"renderStyle\"]);\n const distribution = enumValue(DISTRIBUTIONS, raw[\"distribution\"]);\n const grandStaff = enumValue(GRAND_STAFF_OPTIONS, raw[\"grandStaff\"]);\n const position = enumValue(POSITIONS, raw[\"position\"]);\n const scale = raw[\"scale\"];\n const senzaMisura = enumValue(SENZA_MISURA_DISPLAYS, raw[\"senzaMisura\"]);\n const nonDefaultGroupingDisplay = enumValue(GROUPING_DISPLAYS, raw[\"nonDefaultGroupingDisplay\"]);\n if (renderStyle) settings.renderStyle = renderStyle;\n if (distribution) settings.distribution = distribution;\n if (grandStaff) settings.grandStaff = grandStaff;\n if (position) settings.position = position;\n if (typeof scale === \"number\" && Number.isFinite(scale)) {\n settings.scale = Math.min(12, Math.max(0.25, scale));\n }\n if (senzaMisura) settings.senzaMisura = senzaMisura;\n if (nonDefaultGroupingDisplay) settings.nonDefaultGroupingDisplay = nonDefaultGroupingDisplay;\n return settings;\n}\n\nfunction parseRootTimeSignatures(rootX: Obj | undefined): TimeSignatureStyles | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"timeSignatures\"] as Obj | undefined;\n if (!raw || typeof raw !== \"object\") return undefined;\n const styles: TimeSignatureStyles = {};\n const score = parseTimeSignatureSettings(raw[\"score\"]);\n const parts = parseTimeSignatureSettings(raw[\"parts\"]);\n if (score) styles.score = score;\n if (parts) styles.parts = parts;\n return Object.keys(styles).length > 0 ? styles : undefined;\n}\n\nfunction parseRootChordSymbolStyle(rootX: Obj | undefined): ChordSymbolStyle | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"chordSymbolStyle\"] as Obj | undefined;\n if (!raw) return undefined;\n const style: ChordSymbolStyle = {};\n const rootCase = enumValue([\"uppercase\", \"lowercaseMinor\"] as const, raw[\"rootCase\"]);\n const majorSeventh = enumValue([\"triangle\", \"maj\", \"M\"] as const, raw[\"majorSeventh\"]);\n const minor = enumValue([\"m\", \"min\", \"minus\", \"none\"] as const, raw[\"minor\"]);\n const diminished = enumValue([\"symbol\", \"dim\"] as const, raw[\"diminished\"]);\n const halfDiminished = enumValue([\"symbol\", \"minorFlatFive\"] as const, raw[\"halfDiminished\"]);\n const augmented = enumValue([\"plus\", \"aug\"] as const, raw[\"augmented\"]);\n const extensions = enumValue([\"superscript\", \"baseline\"] as const, raw[\"extensions\"]);\n if (rootCase) style.rootCase = rootCase;\n if (majorSeventh) style.majorSeventh = majorSeventh;\n if (minor) style.minor = minor;\n if (diminished) style.diminished = diminished;\n if (halfDiminished) style.halfDiminished = halfDiminished;\n if (augmented) style.augmented = augmented;\n if (extensions) style.extensions = extensions;\n return Object.keys(style).length > 0 ? style : undefined;\n}\n\nfunction parseRootSoundProfile(rootX: Obj | undefined): SoundProfileAssignment | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const assignment = viritura?.[\"soundProfile\"] as Obj | undefined;\n if (\n !assignment ||\n typeof assignment[\"profileId\"] !== \"string\" ||\n typeof assignment[\"profileVersion\"] !== \"number\" ||\n !Number.isInteger(assignment[\"profileVersion\"]) ||\n assignment[\"profileVersion\"] < 1 ||\n !assignment[\"parts\"] ||\n typeof assignment[\"parts\"] !== \"object\"\n ) {\n return undefined;\n }\n\n const parts: SoundProfileAssignment[\"parts\"] = {};\n for (const [partId, override] of Object.entries(assignment[\"parts\"] as Obj)) {\n const raw = override as Obj | undefined;\n const sourceId = raw?.[\"sourceId\"];\n if (typeof sourceId === \"string\" && sourceId.length > 0) {\n const entry: SoundProfileAssignment[\"parts\"][string] = { sourceId };\n const profileId = raw?.[\"profileId\"];\n if (typeof profileId === \"string\" && profileId.length > 0) entry.profileId = profileId;\n const profileVersion = raw?.[\"profileVersion\"];\n if (typeof profileVersion === \"number\" && Number.isInteger(profileVersion) && profileVersion >= 1) {\n entry.profileVersion = profileVersion;\n }\n parts[partId] = entry;\n }\n }\n return { profileId: assignment[\"profileId\"], profileVersion: assignment[\"profileVersion\"], parts };\n}\n\n/**\n * Parse `_x.viritura.videoSync`.\n *\n * Validation is strict about the offset because it is the one field that\n * silently corrupts the whole feature: a non-numeric offset that defaulted to\n * zero would place every cue at the wrong frame while looking like it worked.\n * Media identity is optional \u2014 a score can remember an offset for a picture the\n * user has not relinked yet.\n */\n/**\n * Parse `_x.viritura.videoSync.hitPoints`.\n *\n * Entries missing an id or a usable time are dropped rather than defaulted: a\n * hit at the wrong second is worse than a hit that is absent, because the\n * solver would silently write the cue against it.\n */\nfunction parseHitPoints(raw: unknown): HitPoint[] {\n if (!Array.isArray(raw)) return [];\n const out: HitPoint[] = [];\n for (const entry of raw) {\n if (typeof entry !== \"object\" || entry === null) continue;\n const item = entry as Obj;\n const id = item[\"id\"];\n const seconds = item[\"pictureSeconds\"];\n if (typeof id !== \"string\" || id.length === 0) continue;\n if (typeof seconds !== \"number\" || !Number.isFinite(seconds) || seconds < 0) continue;\n\n const hit: HitPoint = { id, pictureSeconds: seconds };\n const label = item[\"label\"];\n if (typeof label === \"string\" && label.length > 0) hit.label = label;\n if (item[\"locked\"] === false) hit.locked = false;\n out.push(hit);\n }\n return out;\n}\n\n/** Parse the attached picture's identity, which is optional and all-or-nothing. */\nfunction parseVideoMedia(raw: Obj | undefined): VideoMediaIdentity | undefined {\n const displayName = raw?.[\"displayName\"];\n if (typeof displayName !== \"string\" || displayName.length === 0) return undefined;\n\n const media: VideoMediaIdentity = { displayName };\n const contentHash = raw?.[\"contentHash\"];\n if (typeof contentHash === \"string\" && contentHash.length > 0) media.contentHash = contentHash;\n const demoSourceId = raw?.[\"demoSourceId\"];\n if (typeof demoSourceId === \"string\" && demoSourceId.length > 0) media.demoSourceId = demoSourceId;\n const duration = raw?.[\"durationSeconds\"];\n if (typeof duration === \"number\" && Number.isFinite(duration) && duration > 0) media.durationSeconds = duration;\n return media;\n}\n\nfunction parseRootVideoSync(rootX: Obj | undefined): VideoSyncSettings | undefined {\n const viritura = rootX?.[\"viritura\"] as Obj | undefined;\n const raw = viritura?.[\"videoSync\"] as Obj | undefined;\n if (!raw || typeof raw !== \"object\") return undefined;\n\n const offset = raw[\"pictureOffsetSeconds\"];\n if (typeof offset !== \"number\" || !Number.isFinite(offset)) return undefined;\n\n const version = raw[\"version\"];\n const settings: VideoSyncSettings = {\n version: typeof version === \"number\" && Number.isInteger(version) && version >= 1 ? version : 1,\n pictureOffsetSeconds: offset,\n pictureAudioEnabled: raw[\"pictureAudioEnabled\"] === true,\n };\n\n const startTimecode = raw[\"startTimecodeSeconds\"];\n if (typeof startTimecode === \"number\" && Number.isFinite(startTimecode)) {\n settings.startTimecodeSeconds = startTimecode;\n }\n\n const frameRate = raw[\"frameRate\"];\n if (typeof frameRate === \"string\" && frameRate.length > 0) {\n settings.frameRate = frameRate;\n }\n\n const hitPoints = parseHitPoints(raw[\"hitPoints\"]);\n if (hitPoints.length > 0) settings.hitPoints = hitPoints;\n\n const media = parseVideoMedia(raw[\"media\"] as Obj | undefined);\n if (media) settings.media = media;\n\n return settings;\n}\n\nfunction validateSupportConsistency(\n score: Score,\n support: Support | undefined,\n diagnostics: DiagnosticCollector,\n): void {\n // mnx.support consistency checks (research \u00A71.5):\n // The writer's `support` block declares which authored fields are present.\n // If `useBeams === true` but no measure carries beams, or\n // `useAccidentalDisplay === true` but no note carries accidentalDisplay,\n // the document is internally inconsistent \u2014 emit an info diagnostic.\n if (!support) return;\n if (support.useBeams === true) {\n const hasBeams = score.parts.some((p) => p.measures?.some((m) => Array.isArray(m.beams) && m.beams.length > 0));\n if (!hasBeams) {\n diagnostics.info(\n ptr(\"mnx\", \"support\", \"useBeams\"),\n \"mnx.support.useBeams is true but no measure carries beams; engine will auto-beam\",\n \"support-mismatch\",\n );\n }\n }\n if (support.useAccidentalDisplay === true) {\n const hasAccDisp = score.parts.some((p) =>\n p.measures?.some((m) =>\n m.sequences?.some((seq) =>\n seq.content.some(\n (c) =>\n c.type === \"event\" && Array.isArray(c.notes) && c.notes.some((n) => n.accidentalDisplay !== undefined),\n ),\n ),\n ),\n );\n if (!hasAccDisp) {\n diagnostics.info(\n ptr(\"mnx\", \"support\", \"useAccidentalDisplay\"),\n \"mnx.support.useAccidentalDisplay is true but no note carries accidentalDisplay; engine will recompute\",\n \"support-mismatch\",\n );\n }\n }\n}\n\nfunction parseMnxInternal(json: unknown, diagnostics: DiagnosticCollector): Score {\n const obj = json as Obj;\n\n if (!obj || typeof obj !== \"object\") {\n throw new Error(\"MNX: Expected a JSON object at root\");\n }\n\n // Sanity-check for the standard top-level keys. This is deliberately a loose\n // interoperability heuristic: warn rather than rejecting the document.\n const hasStandardKeys = \"mnx\" in obj || \"global\" in obj || \"parts\" in obj;\n if (!hasStandardKeys) {\n diagnostics.warn(\n \"\",\n \"Document is missing standard MNX top-level keys (mnx/global/parts); may not be a valid MNX file\",\n \"missing-top-level\",\n );\n }\n\n const mnxMeta = obj[\"mnx\"] as Obj | undefined;\n const version = (mnxMeta?.[\"version\"] as number) ?? 1;\n\n // Sanity-check version. We do not enforce a minimum yet because real-world\n // MNX documents do not consistently carry a numeric version; warn on values\n // that look obviously off.\n if (mnxMeta && typeof mnxMeta[\"version\"] !== \"number\") {\n diagnostics.info(ptr(\"mnx\", \"version\"), \"MNX `mnx.version` is not numeric; assuming 1\", \"non-numeric-version\");\n }\n\n const support = parseSupport(mnxMeta?.[\"support\"] as Obj | undefined);\n\n const globalObj = obj[\"global\"] as Obj | undefined;\n const partsArr = obj[\"parts\"] as RawPart[] | undefined;\n const layoutsArr = obj[\"layouts\"] as unknown[] | undefined;\n const scoresArr = obj[\"scores\"] as unknown[] | undefined;\n\n const score: Score = {\n mnx: support ? { version, support } : { version },\n global: {\n measures: parseGlobalMeasures(globalObj?.[\"measures\"] as RawMeasureGlobal[] | undefined),\n },\n parts: parseParts(partsArr),\n };\n\n // Global lyrics\n if (globalObj?.[\"lyrics\"]) {\n score.global.lyrics = parseGlobalLyrics(globalObj[\"lyrics\"] as RawLyricsGlobal);\n }\n\n // Global sounds (drum-kit / GM sound registry)\n if (globalObj?.[\"sounds\"]) {\n score.global.sounds = parseGlobalSounds(globalObj[\"sounds\"] as RawSoundsGlobal);\n }\n\n // Layouts\n if (layoutsArr && layoutsArr.length > 0) {\n score.layouts = layoutsArr.map((l) => parseLayoutDefinition(l as RawSystemLayout));\n }\n\n // Scores\n if (scoresArr && scoresArr.length > 0) {\n score.scores = scoresArr.map((s) => parseScoreDefinition(s as RawScoreDef));\n }\n\n // Root-level vendor extensions: everything under `_x.viritura` on the\n // document root.\n applyRootExtensions(score, obj[\"_x\"] as Obj | undefined);\n\n // Auto-assign IDs to any elements that lack them\n assignMissingIds(score);\n\n validateSupportConsistency(score, support, diagnostics);\n\n // Unknown-field scan (research \u00A71.1 / \u00A71.7). Walks the raw JSON and emits\n // an info diagnostic for any field at a known node type that this parser\n // doesn't recognize. Nested `_x` vendor dicts are skipped \u2014 by convention\n // they may carry anything.\n scanUnknownFields(obj, diagnostics);\n\n return score;\n}\n\n/**\n * Read the `_x.viritura` root dict onto the score.\n *\n * Split out of `parseMnxInternal` for the same reason the serializer splits\n * `serializeRootExtensions`: each extension is an independent concern, and\n * folding them back into the entry point would push it past the complexity\n * budget one root extension at a time.\n */\nfunction applyRootExtensions(score: Score, rootX: Obj | undefined): void {\n const metadata = parseRootMetadata(rootX);\n if (metadata) score.metadata = metadata;\n\n const textStyles = parseRootTextStyles(rootX);\n if (textStyles) score.textStyles = textStyles;\n\n const timeSignatures = parseRootTimeSignatures(rootX);\n if (timeSignatures) score.timeSignatures = timeSignatures;\n const chordSymbolStyle = parseRootChordSymbolStyle(rootX);\n if (chordSymbolStyle) score.chordSymbolStyle = chordSymbolStyle;\n\n const soundProfile = parseRootSoundProfile(rootX);\n if (soundProfile) score.soundProfile = soundProfile;\n\n const videoSync = parseRootVideoSync(rootX);\n if (videoSync) score.videoSync = videoSync;\n\n const lyricWorkflow = parseRootLyricWorkflow(rootX);\n if (lyricWorkflow) score.lyricWorkflow = lyricWorkflow;\n}\n\n/**\n * Walk the parsed score and assign IDs to any elements that lack one.\n * Preserves existing IDs from the source file. Checks for uniqueness.\n *\n * Newly minted IDs are UUID v7 \u2014 the same generator the editor uses for\n * runtime edits (`generateId()` from `@viritura/core`). Re-parsing the\n * same source file will produce *different* IDs each time, which is fine:\n * the assigned IDs are mutated into the in-memory Score and persist via\n * the normal save path. Anything depending on parse output being\n * byte-stable across runs should diff structurally or strip IDs first.\n */\nfunction assignMissingIds(score: Score): void {\n const usedIds = new Set<string>();\n\n // Collect existing IDs first\n for (const gm of score.global.measures) {\n if (gm.id) usedIds.add(gm.id);\n }\n for (const part of score.parts) {\n if (part.id) usedIds.add(part.id);\n for (const pm of part.measures) {\n for (const seq of pm.sequences) {\n assignContentIds(seq.content, usedIds, true);\n }\n }\n }\n\n // Now assign missing IDs.\n for (const gm of score.global.measures) {\n if (!gm.id) gm.id = mintId(usedIds);\n }\n for (const part of score.parts) {\n if (!part.id) part.id = mintId(usedIds);\n for (const pm of part.measures) {\n for (const seq of pm.sequences) {\n assignContentIds(seq.content, usedIds, false);\n }\n }\n }\n}\n\nfunction mintId(usedIds: Set<string>): string {\n let id = generateId();\n while (usedIds.has(id)) id = generateId();\n usedIds.add(id);\n return id;\n}\n\n/**\n * Walk sequence content to collect or assign IDs on events and notes.\n * @param collectOnly - if true, only collect existing IDs; if false, assign missing ones.\n */\nfunction assignContentIds(content: SequenceContent[], usedIds: Set<string>, collectOnly: boolean): void {\n for (const item of content) {\n if (item.type === \"event\") {\n processEventIds(item, usedIds, collectOnly);\n } else if (item.type === \"tuplet\" || item.type === \"grace\") {\n assignContentIds(\n item.type === \"grace\" ? (item.content as SequenceContent[]) : item.content,\n usedIds,\n collectOnly,\n );\n } else if (item.type === \"tremolo\") {\n assignContentIds(item.content as SequenceContent[], usedIds, collectOnly);\n }\n }\n}\n\nfunction processEventIds(\n event: Extract<SequenceContent, { type: \"event\" }>,\n usedIds: Set<string>,\n collectOnly: boolean,\n): void {\n if (collectOnly) {\n if (event.id) usedIds.add(event.id);\n if (!event.notes) return;\n for (const n of event.notes) {\n if (n.id) usedIds.add(n.id);\n }\n return;\n }\n if (!event.id) event.id = mintId(usedIds);\n if (!event.notes) return;\n for (const n of event.notes) {\n if (!n.id) n.id = mintId(usedIds);\n }\n}\n\n// Re-export sub-parsers for direct access\n\n// ---------------------------------------------------------------------------\n// Unknown-field scan\n// ---------------------------------------------------------------------------\n// Per-node-type whitelists. Any other key at that location yields an info\n// diagnostic so importers can surface \"we silently dropped X\" to users.\n// _x (vendor extensions) is allowed everywhere by convention.\nconst KEYS = {\n root: new Set([\"mnx\", \"global\", \"parts\", \"layouts\", \"scores\", \"_x\"]),\n mnxMeta: new Set([\"version\", \"support\"]),\n support: new Set([\"useAccidentalDisplay\", \"useBeams\"]),\n global: new Set([\"measures\", \"lyrics\"]),\n globalLyrics: new Set([\"lineMetadata\", \"lineOrder\"]),\n globalMeasure: new Set([\n \"id\",\n \"number\",\n \"time\",\n \"key\",\n \"barline\",\n \"repeatStart\",\n \"repeatEnd\",\n \"ending\",\n \"tempos\",\n \"segno\",\n \"fine\",\n \"jump\",\n \"_x\",\n ]),\n time: new Set([\"count\", \"unit\", \"display\", \"_x\"]),\n key: new Set([\"fifths\", \"color\", \"_x\"]),\n barline: new Set([\"type\"]),\n ending: new Set([\"duration\", \"numbers\", \"open\", \"color\"]),\n tempo: new Set([\"bpm\", \"value\", \"location\", \"_x\"]),\n tempoNoteValue: new Set([\"base\", \"dots\"]),\n rhythmicPosition: new Set([\"fraction\"]),\n segno: new Set([\"location\", \"glyph\", \"color\"]),\n fine: new Set([\"location\", \"color\"]),\n jump: new Set([\"type\", \"location\"]),\n part: new Set([\"id\", \"name\", \"shortName\", \"staves\", \"transposition\", \"measures\", \"_x\"]),\n transposition: new Set([\"interval\", \"keyFifthsFlipAt\", \"prefersWrittenPitches\"]),\n partMeasure: new Set([\n \"sequences\",\n \"clefs\",\n \"staffConfigs\",\n \"arpeggios\",\n \"nonArpeggios\",\n \"beams\",\n \"dynamics\",\n \"ottavas\",\n \"measureRepeat\",\n \"_x\",\n ]),\n positionedStaffConfig: new Set([\"config\", \"position\", \"staff\", \"id\", \"_c\", \"_x\"]),\n staffConfig: new Set([\"lines\", \"id\", \"_c\", \"_x\"]),\n measureRepeat: new Set([\"number\", \"counter\", \"displayNumber\", \"staffPosition\", \"_c\", \"_x\"]),\n measureRepeatCounter: new Set([\"count\", \"placement\", \"_c\", \"_x\"]),\n arpeggio: new Set([\"id\", \"position\", \"span\", \"direction\", \"arrow\", \"_c\", \"_x\"]),\n nonArpeggio: new Set([\"id\", \"position\", \"span\", \"_c\", \"_x\"]),\n idPair: new Set([\"start\", \"end\"]),\n positionedClef: new Set([\"clef\", \"position\", \"staff\"]),\n clef: new Set([\"sign\", \"staffPosition\", \"color\", \"glyph\", \"octave\", \"showOctave\", \"hide\"]),\n beam: new Set([\"events\", \"beams\", \"direction\"]),\n dynamic: new Set([\"position\", \"value\", \"glyph\", \"staff\", \"voice\"]),\n ottava: new Set([\"position\", \"end\", \"value\", \"staff\", \"voice\"]),\n measureRhythmicPosition: new Set([\"measure\", \"position\"]),\n sequence: new Set([\"content\", \"fullMeasure\", \"staff\", \"voice\", \"directionHint\"]),\n fullMeasure: new Set([\"visualDuration\", \"staffPosition\"]),\n duration: new Set([\"base\", \"dots\"]),\n event: new Set([\n \"type\",\n \"id\",\n \"duration\",\n \"staff\",\n \"rest\",\n \"notes\",\n \"slurs\",\n \"markings\",\n \"fermata\",\n \"lyrics\",\n \"stemDirection\",\n \"smufl\",\n \"_x\",\n ]),\n rest: new Set([\"staffPosition\"]),\n note: new Set([\"pitch\", \"id\", \"ties\", \"accidentalDisplay\", \"written\"]),\n pitch: new Set([\"step\", \"octave\", \"alter\"]),\n tie: new Set([\"target\", \"targetType\", \"side\", \"lv\"]),\n accidentalDisplay: new Set([\"show\", \"force\", \"enclosure\"]),\n accidentalEnclosure: new Set([\"symbol\"]),\n written: new Set([\"diatonicDelta\"]),\n slur: new Set([\"target\", \"side\", \"sideEnd\", \"lineType\", \"startNote\", \"endNote\"]),\n markings: new Set([\n \"staccato\",\n \"staccatissimo\",\n \"spiccato\",\n \"staccatissimoWedge\",\n \"tenuto\",\n \"accent\",\n \"strongAccent\",\n \"tremolo\",\n \"softAccent\",\n \"stress\",\n \"unstress\",\n \"breath\",\n \"bowDirection\",\n \"_x\",\n ]),\n tuplet: new Set([\"type\", \"inner\", \"outer\", \"content\", \"bracket\", \"showNumber\", \"showValue\", \"placement\"]),\n tupletDuration: new Set([\"duration\", \"multiple\"]),\n grace: new Set([\"type\", \"content\", \"graceType\", \"slash\", \"color\"]),\n multiTremolo: new Set([\"type\", \"content\", \"marks\", \"outer\"]),\n space: new Set([\"type\", \"duration\"]),\n layoutDefinition: new Set([\"id\", \"content\"]),\n layoutGroup: new Set([\"type\", \"content\", \"symbol\", \"label\", \"barlineStyle\"]),\n layoutStaff: new Set([\"type\", \"sources\", \"label\", \"labelref\"]),\n layoutSource: new Set([\"part\", \"staff\", \"stem\", \"voice\", \"labelref\"]),\n scoreDefinition: new Set([\"name\", \"layout\", \"multimeasureRests\", \"pages\", \"useWritten\", \"_x\"]),\n multimeasureRest: new Set([\"start\", \"duration\"]),\n page: new Set([\"systems\"]),\n system: new Set([\"measure\", \"layout\", \"layoutChanges\"]),\n layoutChange: new Set([\"layout\", \"location\"]),\n};\n\nfunction scanUnknownFields(json: unknown, dx: DiagnosticCollector): void {\n if (!json || typeof json !== \"object\") return;\n const root = json as Obj;\n emitUnknown(root, KEYS.root, \"\", dx);\n\n const mnxMeta = root[\"mnx\"];\n if (mnxMeta && typeof mnxMeta === \"object\") {\n emitUnknown(mnxMeta as Obj, KEYS.mnxMeta, ptr(\"mnx\"), dx);\n const support = (mnxMeta as Obj)[\"support\"];\n if (support && typeof support === \"object\") {\n emitUnknown(support as Obj, KEYS.support, ptr(\"mnx\", \"support\"), dx);\n }\n }\n\n const global = root[\"global\"];\n if (global && typeof global === \"object\") {\n emitUnknown(global as Obj, KEYS.global, ptr(\"global\"), dx);\n const measures = (global as Obj)[\"measures\"];\n if (Array.isArray(measures)) {\n measures.forEach((gm, i) => scanGlobalMeasure(gm as Obj, ptr(\"global\", \"measures\", i), dx));\n }\n }\n\n const parts = root[\"parts\"];\n if (Array.isArray(parts)) {\n parts.forEach((p, i) => scanPart(p as Obj, ptr(\"parts\", i), dx));\n }\n\n const layouts = root[\"layouts\"];\n if (Array.isArray(layouts)) {\n layouts.forEach((l, i) => scanLayout(l as Obj, ptr(\"layouts\", i), dx));\n }\n\n const scores = root[\"scores\"];\n if (Array.isArray(scores)) {\n scores.forEach((s, i) => {\n if (s && typeof s === \"object\") {\n emitUnknown(s as Obj, KEYS.scoreDefinition, ptr(\"scores\", i), dx);\n }\n });\n }\n}\n\nfunction emitUnknown(obj: Obj, allowed: Set<string>, parentPtr: string, dx: DiagnosticCollector): void {\n for (const k of Object.keys(obj)) {\n if (allowed.has(k)) continue;\n if (k.startsWith(\"_\")) continue; // _x and friends are vendor-private\n dx.info(parentPtr + ptr(k), `Unknown field \"${k}\" dropped during MNX parse`, \"unknown-field\");\n }\n}\n\nfunction scanGlobalMeasure(gm: Obj, p: string, dx: DiagnosticCollector): void {\n if (!gm || typeof gm !== \"object\") return;\n emitUnknown(gm, KEYS.globalMeasure, p, dx);\n const time = gm[\"time\"] as Obj | undefined;\n if (time) emitUnknown(time, KEYS.time, p + ptr(\"time\"), dx);\n const key = gm[\"key\"] as Obj | undefined;\n if (key) emitUnknown(key, KEYS.key, p + ptr(\"key\"), dx);\n const barline = gm[\"barline\"] as Obj | undefined;\n if (barline) emitUnknown(barline, KEYS.barline, p + ptr(\"barline\"), dx);\n const ending = gm[\"ending\"] as Obj | undefined;\n if (ending) emitUnknown(ending, KEYS.ending, p + ptr(\"ending\"), dx);\n const tempos = gm[\"tempos\"];\n if (Array.isArray(tempos)) {\n tempos.forEach((t, i) => {\n if (t && typeof t === \"object\") {\n emitUnknown(t as Obj, KEYS.tempo, p + ptr(\"tempos\", i), dx);\n }\n });\n }\n const segno = gm[\"segno\"] as Obj | undefined;\n if (segno) emitUnknown(segno, KEYS.segno, p + ptr(\"segno\"), dx);\n const fine = gm[\"fine\"] as Obj | undefined;\n if (fine) emitUnknown(fine, KEYS.fine, p + ptr(\"fine\"), dx);\n const jump = gm[\"jump\"] as Obj | undefined;\n if (jump) emitUnknown(jump, KEYS.jump, p + ptr(\"jump\"), dx);\n}\n\nfunction scanPart(part: Obj, p: string, dx: DiagnosticCollector): void {\n if (!part || typeof part !== \"object\") return;\n emitUnknown(part, KEYS.part, p, dx);\n const trans = part[\"transposition\"] as Obj | undefined;\n if (trans) emitUnknown(trans, KEYS.transposition, p + ptr(\"transposition\"), dx);\n const measures = part[\"measures\"];\n if (Array.isArray(measures)) {\n measures.forEach((pm, i) => scanPartMeasure(pm as Obj, p + ptr(\"measures\", i), dx));\n }\n}\n\nfunction scanPartMeasure(pm: Obj, p: string, dx: DiagnosticCollector): void {\n if (!pm || typeof pm !== \"object\") return;\n emitUnknown(pm, KEYS.partMeasure, p, dx);\n const sequences = pm[\"sequences\"];\n if (Array.isArray(sequences)) {\n sequences.forEach((s, i) => scanSequence(s as Obj, p + ptr(\"sequences\", i), dx));\n }\n const beams = pm[\"beams\"];\n if (Array.isArray(beams)) {\n beams.forEach((b, i) => {\n if (b && typeof b === \"object\") {\n emitUnknown(b as Obj, KEYS.beam, p + ptr(\"beams\", i), dx);\n }\n });\n }\n const arpeggios = pm[\"arpeggios\"];\n if (Array.isArray(arpeggios)) {\n arpeggios.forEach((a, i) => scanArpeggioObject(a as Obj, p + ptr(\"arpeggios\", i), dx));\n }\n const nonArpeggios = pm[\"nonArpeggios\"];\n if (Array.isArray(nonArpeggios)) {\n nonArpeggios.forEach((a, i) => scanNonArpeggioObject(a as Obj, p + ptr(\"nonArpeggios\", i), dx));\n }\n const measureRepeat = pm[\"measureRepeat\"] as Obj | undefined;\n if (measureRepeat && typeof measureRepeat === \"object\") {\n emitUnknown(measureRepeat, KEYS.measureRepeat, p + ptr(\"measureRepeat\"), dx);\n const counter = measureRepeat[\"counter\"] as Obj | undefined;\n if (counter && typeof counter === \"object\") {\n emitUnknown(counter, KEYS.measureRepeatCounter, p + ptr(\"measureRepeat\") + ptr(\"counter\"), dx);\n }\n const staffConfigs = pm[\"staffConfigs\"];\n if (Array.isArray(staffConfigs)) {\n staffConfigs.forEach((staffConfig, i) => {\n if (!staffConfig || typeof staffConfig !== \"object\") return;\n emitUnknown(staffConfig as Obj, KEYS.positionedStaffConfig, p + ptr(\"staffConfigs\", i), dx);\n const config = (staffConfig as Obj)[\"config\"] as Obj | undefined;\n if (config && typeof config === \"object\") {\n emitUnknown(config, KEYS.staffConfig, p + ptr(\"staffConfigs\", i) + ptr(\"config\"), dx);\n }\n });\n }\n }\n}\n\nfunction scanArpeggioObject(obj: Obj, p: string, dx: DiagnosticCollector): void {\n if (!obj || typeof obj !== \"object\") return;\n emitUnknown(obj, KEYS.arpeggio, p, dx);\n const position = obj[\"position\"] as Obj | undefined;\n if (position) emitUnknown(position, KEYS.rhythmicPosition, p + ptr(\"position\"), dx);\n const span = obj[\"span\"] as Obj | undefined;\n if (span) emitUnknown(span, KEYS.idPair, p + ptr(\"span\"), dx);\n}\n\nfunction scanNonArpeggioObject(obj: Obj, p: string, dx: DiagnosticCollector): void {\n if (!obj || typeof obj !== \"object\") return;\n emitUnknown(obj, KEYS.nonArpeggio, p, dx);\n const position = obj[\"position\"] as Obj | undefined;\n if (position) emitUnknown(position, KEYS.rhythmicPosition, p + ptr(\"position\"), dx);\n const span = obj[\"span\"] as Obj | undefined;\n if (span) emitUnknown(span, KEYS.idPair, p + ptr(\"span\"), dx);\n}\n\nfunction scanSequence(seq: Obj, p: string, dx: DiagnosticCollector): void {\n if (!seq || typeof seq !== \"object\") return;\n emitUnknown(seq, KEYS.sequence, p, dx);\n const content = seq[\"content\"];\n if (Array.isArray(content)) {\n content.forEach((c, i) => scanContentItem(c as Obj, p + ptr(\"content\", i), dx));\n }\n}\n\nfunction scanContentItem(c: Obj, p: string, dx: DiagnosticCollector): void {\n if (!c || typeof c !== \"object\") return;\n // MNX `type` is required on tuplet/grace/space/tremolo, but optional on event\n // (event is the default content kind when no discriminator is present).\n const type = c[\"type\"] ?? \"event\";\n if (type === \"event\") {\n emitUnknown(c, KEYS.event, p, dx);\n const notes = c[\"notes\"];\n if (Array.isArray(notes)) {\n notes.forEach((n, i) => {\n if (n && typeof n === \"object\") {\n emitUnknown(n as Obj, KEYS.note, p + ptr(\"notes\", i), dx);\n }\n });\n }\n const markings = c[\"markings\"] as Obj | undefined;\n if (markings) emitUnknown(markings, KEYS.markings, p + ptr(\"markings\"), dx);\n } else if (type === \"tuplet\") {\n emitUnknown(c, KEYS.tuplet, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"grace\") {\n emitUnknown(c, KEYS.grace, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"tremolo\") {\n emitUnknown(c, KEYS.multiTremolo, p, dx);\n const inner = c[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanContentItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"space\") {\n emitUnknown(c, KEYS.space, p, dx);\n }\n}\n\nfunction scanLayout(l: Obj, p: string, dx: DiagnosticCollector): void {\n if (!l || typeof l !== \"object\") return;\n emitUnknown(l, KEYS.layoutDefinition, p, dx);\n const content = l[\"content\"];\n if (Array.isArray(content)) {\n content.forEach((it, i) => scanLayoutItem(it as Obj, p + ptr(\"content\", i), dx));\n }\n}\n\nfunction scanLayoutItem(it: Obj, p: string, dx: DiagnosticCollector): void {\n if (!it || typeof it !== \"object\") return;\n const type = it[\"type\"];\n if (type === \"group\") {\n emitUnknown(it, KEYS.layoutGroup, p, dx);\n const inner = it[\"content\"];\n if (Array.isArray(inner)) {\n inner.forEach((ci, i) => scanLayoutItem(ci as Obj, p + ptr(\"content\", i), dx));\n }\n } else if (type === \"staff\") {\n emitUnknown(it, KEYS.layoutStaff, p, dx);\n const sources = it[\"sources\"];\n if (Array.isArray(sources)) {\n sources.forEach((s, i) => {\n if (s && typeof s === \"object\") {\n emitUnknown(s as Obj, KEYS.layoutSource, p + ptr(\"sources\", i), dx);\n }\n });\n }\n }\n}\n", "// MNX Parser \u2014 parseGlobal (split from parser.ts)\n//\n// Schema-defined inputs (sounds, lyrics, measure-global, time, key,\n// barline, ending, tempo, segno, fine, jump, dynamic-group, ottava,\n// rhythmic-position, measure-rhythmic-position, repeat-start, repeat-end)\n// now consume the generated Raw* types \u2014 schema drift surfaces as a\n// compile error here.\n//\n// Viritura vendor extensions (pedal, chordSymbol, textExpression,\n// coda) are NOT defined in the MNX schema, so their inputs remain\n// Record<string, unknown>. See docs/spec/viritura-extensions.md for\n// the layered types.\n\nimport type { GlobalLyrics, LyricLineMetadataEntry } from \"@viritura/core\";\nimport type { Sound } from \"@viritura/core\";\nimport { generateId } from \"@viritura/core\";\nimport type {\n GlobalMeasure,\n RhythmicPosition,\n Tempo,\n TempoNoteValue,\n DynamicGroup,\n DynamicGroupBase,\n MeasureRhythmicPosition,\n Ottava,\n Pedal,\n PedalType,\n PedalLineStyle,\n Segno,\n Fine,\n Jump,\n Coda,\n GradualTempo,\n GradualTempoKind,\n RepeatStart,\n RepeatEnd,\n Ending,\n TextExpression,\n ExpressionPlacement,\n ChordSymbol,\n ChordRoot,\n} from \"@viritura/core\";\nimport type { TimeSignature } from \"@viritura/core\";\nimport type { KeySignature } from \"@viritura/core\";\nimport type { Barline } from \"@viritura/core\";\nimport type { NoteValueBase, BarlineType } from \"@viritura/core\";\n\nimport type {\n SoundsGlobal as RawSoundsGlobal,\n LyricsGlobal as RawLyricsGlobal,\n MeasureGlobal as RawMeasureGlobal,\n RhythmicPosition as RawRhythmicPosition,\n MeasureRhythmicPosition as RawMeasureRhythmicPosition,\n Tempo as RawTempo,\n NoteValue as RawNoteValue,\n DynamicGroup as RawDynamicGroup,\n Ottava as RawOttava,\n Segno as RawSegno,\n Fine as RawFine,\n Jump as RawJump,\n Ending as RawEnding,\n RepeatStart as RawRepeatStart,\n RepeatEnd as RawRepeatEnd,\n Time as RawTime,\n Key as RawKey,\n Barline as RawBarline,\n} from \"@viritura/core/raw\";\nimport type {\n DynamicGroupExtensions as RawDynamicGroupExt,\n MeasureGlobalExtensions as RawMeasureGlobalExt,\n TimeExtensions as RawTimeExtensions,\n Pedal as RawPedal,\n ChordSymbol as RawChordSymbol,\n ChordRoot as RawChordRoot,\n TextExpression as RawTextExpression,\n Coda as RawCoda,\n Jump as RawVendorJump,\n GradualTempo as RawGradualTempo,\n} from \"@viritura/core/raw-viritura\";\n\n/** Untyped vendor-extension payload \u2014 used only for fields outside the\n * extensions schema (e.g. legacy `times` on repeat-start, atonal key flag). */\ntype VendorObj = Record<string, unknown>;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global sounds (drum-kit / GM sound registry)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalSounds(raw: RawSoundsGlobal): Record<string, Sound> {\n const out: Record<string, Sound> = {};\n for (const key of Object.keys(raw)) {\n const s = raw[key]!;\n const sound: Sound = {};\n if (typeof s.midiNumber === \"number\") sound.midiNumber = s.midiNumber;\n if (typeof s.name === \"string\") sound.name = s.name;\n if (typeof s.id === \"string\") sound.id = s.id;\n out[key] = sound;\n }\n return out;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global lyrics\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalLyrics(raw: RawLyricsGlobal): GlobalLyrics {\n const result: GlobalLyrics = {};\n if (raw.lineMetadata) {\n const entries: Record<string, LyricLineMetadataEntry> = {};\n for (const key of Object.keys(raw.lineMetadata)) {\n const entry = raw.lineMetadata[key]!;\n const parsed: LyricLineMetadataEntry = {};\n if (entry.label) parsed.label = entry.label;\n if (entry.lang) parsed.lang = entry.lang;\n entries[key] = parsed;\n }\n result.lineMetadata = entries;\n }\n if (raw.lineOrder) {\n result.lineOrder = raw.lineOrder;\n }\n return result;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Global measures\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseGlobalMeasures(arr: RawMeasureGlobal[] | undefined): GlobalMeasure[] {\n if (!arr) return [];\n return arr.map(parseGlobalMeasure);\n}\n\nfunction parseGlobalMeasure(raw: RawMeasureGlobal): GlobalMeasure {\n const measure: GlobalMeasure = {};\n\n if (raw.id) measure.id = raw.id;\n if (raw.number !== undefined) measure.number = raw.number;\n\n if (raw.time) measure.time = parseTimeSignature(raw.time);\n if (raw.key) measure.key = parseKeySignature(raw.key);\n if (raw.barline) measure.barline = parseBarline(raw.barline);\n\n if (raw.repeatStart !== undefined) {\n measure.repeatStart = parseRepeatStart(raw.repeatStart);\n }\n if (raw.repeatEnd !== undefined) {\n measure.repeatEnd = parseRepeatEnd(raw.repeatEnd);\n }\n if (raw.ending !== undefined) {\n measure.ending = parseEnding(raw.ending);\n }\n if (raw.tempos) {\n measure.tempos = raw.tempos.map(parseTempo);\n }\n if (raw.segno) measure.segno = parseSegno(raw.segno);\n if (raw.fine) measure.fine = parseFine(raw.fine);\n if (raw.jump) measure.jump = parseJump(raw.jump);\n\n // _x.viritura vendor extensions on the global measure\n const viritura = raw._x?.[\"viritura\"] as RawMeasureGlobalExt | undefined;\n if (viritura) {\n if (viritura.senzaMisura && measure.time) measure.time.display = \"senzaMisura\";\n if (viritura.rehearsalMark) {\n const rm = viritura.rehearsalMark;\n measure.rehearsalMark = { text: rm.text };\n if (rm.style) measure.rehearsalMark.style = rm.style;\n applyManualPlacement(measure.rehearsalMark, rm as VendorObj);\n }\n if (viritura.coda) {\n measure.coda = parseCoda(viritura.coda);\n }\n if (viritura.jump) {\n measure.jump = parseJump(viritura.jump);\n }\n if (viritura.gradualTempo) {\n measure.gradualTempo = parseGradualTempo(viritura.gradualTempo);\n }\n if (viritura.chordSymbols) {\n measure.chordSymbols = viritura.chordSymbols.map(parseChordSymbol);\n }\n }\n\n return measure;\n}\n\nfunction parseRepeatStart(raw: RawRepeatStart): RepeatStart {\n const r: RepeatStart = {};\n // `times` is an extension on top of repeat-start (schema only requires\n // global-attrs); keep the existing parsing behavior.\n const times = (raw as VendorObj)[\"times\"];\n if (typeof times === \"number\") r.times = times;\n return r;\n}\n\nfunction parseRepeatEnd(raw: RawRepeatEnd): RepeatEnd {\n const r: RepeatEnd = {};\n if (raw.times !== undefined) r.times = raw.times;\n return r;\n}\n\nfunction parseEnding(raw: RawEnding): Ending {\n const ending: Ending = {\n duration: raw.duration,\n numbers: (raw.numbers ?? []) as number[],\n };\n if (raw.open !== undefined) ending.open = raw.open;\n if (raw.color) ending.color = raw.color;\n return ending;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Direction types (tempo, dynamic, ottava, jump markers)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Read `manualOffset` ([dx, dy]) and `avoidCollisions` (bool) from a vendor\n * object onto a target that carries those optional fields. Shared by tempo /\n * dynamic / rehearsal / expression. */\nfunction applyManualPlacement(\n target: { manualOffset?: [number, number]; avoidCollisions?: boolean },\n src: VendorObj | undefined,\n): void {\n if (!src) return;\n const mo = src[\"manualOffset\"];\n if (Array.isArray(mo) && mo.length === 2) {\n const [dx, dy] = mo;\n if (typeof dx === \"number\" && typeof dy === \"number\") target.manualOffset = [dx, dy];\n }\n if (typeof src[\"avoidCollisions\"] === \"boolean\") target.avoidCollisions = src[\"avoidCollisions\"];\n}\n\nexport function parseRhythmicPosition(raw: RawRhythmicPosition): RhythmicPosition {\n // Schema types fraction as integer-unsigned[]; decoded model wants tuple.\n return { fraction: raw.fraction as [number, number] };\n}\n\nfunction parseTempo(raw: RawTempo): Tempo {\n const tempo: Tempo = {\n bpm: raw.bpm,\n value: parseTempoNoteValue(raw.value),\n };\n if (raw.location) {\n tempo.location = parseRhythmicPosition(raw.location);\n }\n // _x.viritura vendor extensions on the tempo object\n // (tempo-extensions is not in the schema yet \u2014 keep narrow casts.)\n const viritura = raw._x?.[\"viritura\"] as VendorObj | undefined;\n if (viritura) {\n if (typeof viritura[\"text\"] === \"string\") tempo.text = viritura[\"text\"];\n if (typeof viritura[\"showMetronomeMark\"] === \"boolean\")\n tempo.showMetronomeMark = viritura[\"showMetronomeMark\"] as boolean;\n if (typeof viritura[\"showText\"] === \"boolean\") tempo.showText = viritura[\"showText\"] as boolean;\n applyManualPlacement(tempo, viritura);\n }\n return tempo;\n}\n\nfunction parseTempoNoteValue(raw: RawNoteValue): TempoNoteValue {\n const tnv: TempoNoteValue = {\n base: raw.base as NoteValueBase,\n };\n if (raw.dots !== undefined) tnv.dots = raw.dots;\n return tnv;\n}\n\n/**\n * Dynamic-group fields that copy across unchanged when present. Listing them\n * keeps the parser's branch count flat as the spec grows.\n */\nconst DYNAMIC_GROUP_SCALAR_FIELDS = [\n \"value\",\n \"residualValue\",\n \"accentPrefix\",\n \"accentSuffix\",\n \"placement\",\n \"prefix\",\n \"relativeValue\",\n \"staff\",\n \"staffEnd\",\n \"suffix\",\n \"visuallyContinues\",\n \"voice\",\n \"wedgeType\",\n] as const satisfies readonly (keyof DynamicGroupBase & keyof RawDynamicGroup)[];\n\nexport function parseDynamicGroup(raw: RawDynamicGroup): DynamicGroup {\n const common: Omit<DynamicGroupBase, \"type\"> = {\n id: raw.id ?? generateId(),\n position: parseRhythmicPosition(raw.position),\n };\n for (const field of DYNAMIC_GROUP_SCALAR_FIELDS) {\n const value = raw[field];\n if (value !== undefined) Object.assign(common, { [field]: value });\n }\n if (raw.end !== undefined) common.end = parseMeasureRhythmicPosition(raw.end);\n if (raw.glyphs !== undefined) common.glyphs = [...raw.glyphs];\n applyManualPlacement(common, raw._x?.[\"viritura\"] as RawDynamicGroupExt | undefined);\n\n switch (raw.type) {\n case \"immediate\":\n if (raw.value === undefined) throw new Error(`Dynamic group ${common.id} of type immediate requires value`);\n return { ...common, type: \"immediate\", value: raw.value };\n case \"gradual\":\n if (common.end === undefined || raw.wedgeType === undefined) {\n throw new Error(`Dynamic group ${common.id} of type gradual requires end and wedgeType`);\n }\n return { ...common, type: \"gradual\", end: common.end, wedgeType: raw.wedgeType };\n case \"relative\":\n if (raw.relativeValue === undefined) {\n throw new Error(`Dynamic group ${common.id} of type relative requires relativeValue`);\n }\n return { ...common, type: \"relative\", relativeValue: raw.relativeValue };\n case \"accent\":\n if (raw.value === undefined) throw new Error(`Dynamic group ${common.id} of type accent requires value`);\n return { ...common, type: \"accent\", value: raw.value };\n }\n}\n\nfunction parseMeasureRhythmicPosition(raw: RawMeasureRhythmicPosition): MeasureRhythmicPosition {\n return {\n measure: raw.measure,\n position: parseRhythmicPosition(raw.position),\n };\n}\n\nexport function parseOttava(raw: RawOttava): Ottava {\n const ott: Ottava = {\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n value: raw.value,\n };\n if (raw.placement !== undefined) ott.placement = raw.placement;\n if (raw.staff !== undefined) ott.staff = raw.staff;\n if (raw.voice) ott.voice = raw.voice;\n return ott;\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Vendor-extension parsers\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport function parsePedal(raw: RawPedal): Pedal {\n const pedal: Pedal = {\n type: raw.type as PedalType,\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n };\n if (raw.style) pedal.style = raw.style as PedalLineStyle;\n if (raw.staff !== undefined) pedal.staff = raw.staff;\n if (raw.voice) pedal.voice = raw.voice;\n return pedal;\n}\n\nfunction parseChordRoot(raw: RawChordRoot): ChordRoot {\n const root: ChordRoot = {\n step: raw.step,\n };\n if (raw.alter !== undefined) root.alter = raw.alter;\n return root;\n}\n\nfunction parseChordSymbol(raw: RawChordSymbol): ChordSymbol {\n const cs: ChordSymbol = {\n position: parseRhythmicPosition(raw.position),\n };\n if (raw.root !== undefined) cs.root = parseChordRoot(raw.root);\n if (raw.rawText !== undefined) cs.rawText = raw.rawText;\n if (raw.quality !== undefined) cs.quality = raw.quality;\n if (raw.kindText !== undefined) cs.kindText = raw.kindText;\n if (raw.bass) cs.bass = parseChordRoot(raw.bass);\n if (raw.extension !== undefined) cs.extension = raw.extension;\n if (raw.textOverride !== undefined) cs.textOverride = raw.textOverride;\n return cs;\n}\n\nexport function parseTextExpression(raw: RawTextExpression): TextExpression {\n const expr: TextExpression = {\n text: raw.text,\n position: parseRhythmicPosition(raw.position),\n };\n if (raw.placement) expr.placement = raw.placement as ExpressionPlacement;\n if (raw.staff !== undefined) expr.staff = raw.staff;\n if (raw.voice) expr.voice = raw.voice;\n const mo = raw.manualOffset;\n if (Array.isArray(mo) && mo.length === 2) {\n const [dx, dy] = mo;\n if (typeof dx === \"number\" && typeof dy === \"number\") expr.manualOffset = [dx, dy];\n }\n if (typeof raw.avoidCollisions === \"boolean\") expr.avoidCollisions = raw.avoidCollisions;\n return expr;\n}\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Schema direction markers (segno / fine / jump / coda)\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction parseSegno(raw: RawSegno): Segno {\n const segno: Segno = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.glyph) segno.glyph = raw.glyph;\n if (raw.color) segno.color = raw.color;\n return segno;\n}\n\nfunction parseFine(raw: RawFine): Fine {\n const fine: Fine = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.color) fine.color = raw.color;\n return fine;\n}\n\nfunction parseJump(raw: RawJump | RawVendorJump): Jump {\n return {\n type: raw.type as \"segno\" | \"dsalfine\" | \"dsalcoda\" | \"dcalcoda\",\n location: parseRhythmicPosition(raw.location),\n };\n}\n\n/** Coda is a Viritura vendor extension (no MNX schema). */\nfunction parseCoda(raw: RawCoda): Coda {\n const coda: Coda = {\n location: parseRhythmicPosition(raw.location),\n };\n if (raw.glyph) coda.glyph = raw.glyph;\n if (raw.color) coda.color = raw.color;\n return coda;\n}\n\n/** Gradual tempo (rit./accel.) is a Viritura vendor extension (no MNX schema). */\nfunction parseGradualTempo(raw: RawGradualTempo): GradualTempo {\n const gt: GradualTempo = {\n position: parseRhythmicPosition(raw.position),\n end: parseMeasureRhythmicPosition(raw.end),\n endBpm: raw.endBpm,\n };\n if (raw.startBpm !== undefined) gt.startBpm = raw.startBpm;\n if (raw.kind) gt.kind = raw.kind as GradualTempoKind;\n return gt;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Simple types\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseTimeSignature(raw: RawTime): TimeSignature {\n const ts: TimeSignature = {\n count: raw.count ?? 4,\n unit: raw.unit ?? 4,\n };\n if (raw.display === \"common\" || raw.display === \"cut\") {\n ts.display = raw.display;\n }\n const viritura = raw._x?.[\"viritura\"] as RawTimeExtensions | undefined;\n if (viritura?.beatStructure) ts.beatStructure = [...viritura.beatStructure];\n if (viritura?.groupingDisplay) ts.groupingDisplay = viritura.groupingDisplay;\n if (viritura?.display === \"note\") ts.display = \"note\";\n return ts;\n}\n\nfunction parseKeySignature(raw: RawKey): KeySignature {\n const key: KeySignature = {\n fifths: raw.fifths ?? 0,\n };\n if (raw.color) key.color = raw.color;\n const viritura = raw._x?.[\"viritura\"] as VendorObj | undefined;\n if (viritura?.[\"atonal\"]) key.atonal = true;\n return key;\n}\n\nfunction parseBarline(raw: RawBarline): Barline {\n return {\n type: (raw.type as BarlineType) ?? \"regular\",\n };\n}\n", "// MNX Parser \u2014 parseParts (split from parser.ts)\n//\n// Schema-defined inputs (Part, PartMeasure, Kit, KitComponent, Beam,\n// Arpeggio, NonArpeggio, PositionedClef) now consume the generated\n// Raw* types. The vendor-extension blocks under `_x.viritura` (pedal,\n// pedal, chordSymbol, textExpression, condensingOverride, transposition,\n// part identity) are still narrowed with `as Record<string, unknown>`\n// because the schema deliberately leaves vendor dicts opaque.\n\nimport type {\n PartMeasure,\n Beam,\n PartMeasureArpeggio,\n NonArpeggio,\n MeasureRepeat,\n MeasureRepeatCounter,\n StaffMeter,\n StaffMeterChange,\n} from \"@viritura/core\";\nimport type { Part } from \"@viritura/core\";\nimport type { PositionedClef, Clef } from \"@viritura/core\";\nimport type { ClefSign, KitComponent, NoteheadShape } from \"@viritura/core\";\n\nimport type {\n Part as RawPart,\n PartMeasure as RawPartMeasure,\n Kit as RawKit,\n KitComponent as RawKitComponent,\n Beam as RawBeam,\n Arpeggio as RawArpeggio,\n NonArpeggio as RawNonArpeggio,\n MeasureRepeat as RawMeasureRepeat,\n PositionedClef as RawPositionedClef,\n PositionedStaffConfig as RawPositionedStaffConfig,\n StaffConfig as RawStaffConfig,\n IdPair as RawIdPair,\n} from \"@viritura/core/raw\";\nimport type {\n PartExtensions as RawPartExt,\n PartMeasureExtensions as RawPartMeasureExt,\n KitComponentExtensions as RawKitComponentExt,\n StaffMeterChange as RawStaffMeterChange,\n} from \"@viritura/core/raw-viritura\";\n\nimport { parseDynamicGroup, parseOttava, parseTextExpression, parsePedal, parseRhythmicPosition } from \"./parseGlobal\";\nimport { parseSequences } from \"./parseContent\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Parts\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseParts(arr: RawPart[] | undefined): Part[] {\n if (!arr) return [];\n return arr.map(parsePart);\n}\n\nfunction parsePart(raw: RawPart): Part {\n const part: Part = {\n name: raw.name ?? \"\",\n measures: parsePartMeasures(raw.measures),\n };\n if (raw.id) part.id = raw.id;\n if (raw.shortName !== undefined) part.shortName = raw.shortName;\n if (raw.staves !== undefined) part.staves = raw.staves;\n if (raw.transposition) {\n const t = raw.transposition;\n part.transposition = {\n interval: {\n halfSteps: t.interval.halfSteps,\n staffDistance: t.interval.staffDistance,\n },\n };\n if (t.keyFifthsFlipAt !== undefined) part.transposition.keyFifthsFlipAt = t.keyFifthsFlipAt;\n if (t.prefersWrittenPitches !== undefined) part.transposition.prefersWrittenPitches = t.prefersWrittenPitches;\n }\n if (raw.kit) {\n part.kit = parseKit(raw.kit);\n }\n // _x.viritura vendor extensions (instrument identity + spatial placement)\n const viritura = raw._x?.[\"viritura\"] as RawPartExt | undefined;\n if (viritura) {\n if (viritura.chordSymbolVisibility !== undefined) {\n part.chordSymbolVisibility = viritura.chordSymbolVisibility;\n }\n const ext: { instrumentId?: string; midiProgram?: number; family?: string; spatial?: { x: number; y: number } } =\n {};\n if (typeof viritura.instrumentId === \"string\") ext.instrumentId = viritura.instrumentId;\n if (typeof viritura.midiProgram === \"number\") ext.midiProgram = viritura.midiProgram;\n if (typeof viritura.family === \"string\") ext.family = viritura.family;\n const sp = viritura.spatial;\n if (sp && typeof sp.x === \"number\" && typeof sp.y === \"number\") {\n ext.spatial = { x: sp.x, y: sp.y };\n }\n if (Object.keys(ext).length > 0) {\n part._x = { viritura: ext };\n }\n }\n return part;\n}\n\nfunction parseKit(raw: RawKit): Record<string, KitComponent> {\n const result: Record<string, KitComponent> = {};\n for (const [id, value] of Object.entries(raw)) {\n result[id] = parseKitComponent(value);\n }\n return result;\n}\n\nfunction parseKitComponent(raw: RawKitComponent): KitComponent {\n const c: KitComponent = {\n staffPosition: raw.staffPosition ?? 0,\n };\n if (typeof raw.name === \"string\") c.name = raw.name;\n if (typeof raw.sound === \"string\") c.sound = raw.sound;\n // _x.viritura vendor extensions: notehead (MNX issue #249) + drumKit override.\n const viritura = raw._x?.[\"viritura\"] as RawKitComponentExt | undefined;\n if (viritura?.notehead) {\n c.notehead = viritura.notehead as NoteheadShape;\n }\n if (typeof viritura?.drumKit === \"number\") {\n c.drumKit = viritura.drumKit;\n }\n return c;\n}\n\nfunction parsePartMeasures(arr: RawPartMeasure[] | undefined): PartMeasure[] {\n if (!arr) return [];\n return arr.map(parsePartMeasure);\n}\n\nfunction parsePartMeasure(raw: RawPartMeasure): PartMeasure {\n const pm: PartMeasure = {\n sequences: parseSequences(raw.sequences),\n };\n const clefs = parseClefs(raw.clefs);\n if (clefs !== undefined) pm.clefs = clefs;\n const staffConfigs = parseStaffConfigs(raw.staffConfigs);\n if (staffConfigs !== undefined) pm.staffConfigs = staffConfigs;\n if (raw.beams) {\n pm.beams = raw.beams.map(parseBeam);\n }\n if (raw.arpeggios) {\n pm.arpeggios = raw.arpeggios.map(parseArpeggio);\n }\n if (raw.nonArpeggios) {\n pm.nonArpeggios = raw.nonArpeggios.map(parseNonArpeggio);\n }\n if (raw.dynamics) {\n pm.dynamics = raw.dynamics.map(parseDynamicGroup);\n }\n if (raw.ottavas) {\n pm.ottavas = raw.ottavas.map(parseOttava);\n }\n if (raw.measureRepeat) {\n pm.measureRepeat = parseMeasureRepeat(raw.measureRepeat);\n }\n\n // _x.viritura vendor extensions (none of these are in the MNX schema)\n const viritura = raw._x?.[\"viritura\"] as RawPartMeasureExt | undefined;\n if (viritura) {\n if (viritura.pedals) {\n pm.pedals = viritura.pedals.map(parsePedal);\n }\n if (viritura.expressions) {\n pm.expressions = viritura.expressions.map(parseTextExpression);\n }\n if (viritura.condensingOverride) {\n pm.condensingOverride = viritura.condensingOverride;\n }\n if (viritura.groupingDisplayOverrides && viritura.groupingDisplayOverrides.length > 0) {\n pm.groupingDisplayOverrides = viritura.groupingDisplayOverrides.map((override) => ({\n staff: override.staff,\n groupingDisplay: override.groupingDisplay,\n }));\n }\n if (viritura.staffMeters && viritura.staffMeters.length > 0) {\n pm.staffMeters = viritura.staffMeters.map(parseStaffMeterChange);\n }\n }\n return pm;\n}\n\nfunction parseStaffMeterChange(raw: RawStaffMeterChange): StaffMeterChange {\n if (\"useGlobal\" in raw) {\n return { staff: raw.staff, useGlobal: true };\n }\n const meter: StaffMeter = { count: raw.meter.count, unit: raw.meter.unit };\n if (raw.meter.beatStructure && raw.meter.beatStructure.length > 0) {\n meter.beatStructure = [...raw.meter.beatStructure];\n }\n return { staff: raw.staff, meter, synchronization: raw.synchronization };\n}\n\nfunction parseMeasureRepeat(raw: RawMeasureRepeat): MeasureRepeat {\n const measureRepeat: MeasureRepeat = { number: raw.number };\n if (raw.counter !== undefined) {\n const counter: MeasureRepeatCounter = { count: raw.counter.count };\n if (raw.counter.placement !== undefined) counter.placement = raw.counter.placement;\n measureRepeat.counter = counter;\n }\n if (raw.displayNumber !== undefined) measureRepeat.displayNumber = raw.displayNumber;\n if (raw.staffPosition !== undefined) measureRepeat.staffPosition = raw.staffPosition;\n return measureRepeat;\n}\n\nfunction parseArpeggio(raw: RawArpeggio): PartMeasureArpeggio {\n const arpeggio: PartMeasureArpeggio = {\n position: parseRhythmicPosition(raw.position),\n span: parseIdPair(raw.span),\n };\n if (raw.direction !== undefined) arpeggio.direction = raw.direction;\n if (raw.arrow !== undefined) arpeggio.arrow = raw.arrow;\n if (raw.id !== undefined) arpeggio.id = raw.id;\n return arpeggio;\n}\n\nfunction parseNonArpeggio(raw: RawNonArpeggio): NonArpeggio {\n const nonArpeggio: NonArpeggio = {\n position: parseRhythmicPosition(raw.position),\n span: parseIdPair(raw.span),\n };\n if (raw.id !== undefined) nonArpeggio.id = raw.id;\n return nonArpeggio;\n}\n\nfunction parseIdPair(raw: RawIdPair): { start: string; end: string } {\n return {\n start: raw.start,\n end: raw.end,\n };\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Beams\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseBeam(raw: RawBeam): Beam {\n const beam: Beam = {\n events: raw.events,\n };\n if (raw.beams && raw.beams.length > 0) {\n // Inner beams are `beam-list` (recursive) \u2014 same shape as outer beams.\n beam.beams = raw.beams.map((b) => parseBeam(b as RawBeam));\n }\n if (raw.direction) {\n beam.direction = raw.direction;\n }\n return beam;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Clefs\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseClefs(arr: RawPositionedClef[] | undefined): PositionedClef[] | undefined {\n if (!arr || arr.length === 0) return undefined;\n return arr.map(parsePositionedClef);\n}\n\nfunction parsePositionedClef(raw: RawPositionedClef): PositionedClef {\n const rawClef = raw.clef;\n const clef: Clef = {\n sign: (rawClef.sign as ClefSign) ?? \"G\",\n staffPosition: rawClef.staffPosition ?? -2,\n };\n if (rawClef.color) clef.color = rawClef.color;\n if (rawClef.glyph) clef.glyph = rawClef.glyph;\n if (rawClef.octave !== undefined) clef.octave = rawClef.octave;\n if (rawClef.showOctave !== undefined) clef.showOctave = rawClef.showOctave;\n if (rawClef.hide !== undefined) clef.hide = rawClef.hide;\n\n const result: PositionedClef = { clef };\n\n if (raw.position) {\n if (raw.position.fraction) {\n result.position = { fraction: raw.position.fraction as [number, number] };\n }\n }\n\n if (raw.staff !== undefined) {\n result.staff = raw.staff;\n }\n\n return result;\n}\n\nfunction parseStaffConfigs(arr: RawPositionedStaffConfig[] | undefined): PartMeasure[\"staffConfigs\"] | undefined {\n if (!arr || arr.length === 0) return undefined;\n return arr.map(parsePositionedStaffConfig);\n}\n\nfunction parsePositionedStaffConfig(raw: RawPositionedStaffConfig): NonNullable<PartMeasure[\"staffConfigs\"]>[number] {\n const config: RawStaffConfig = { ...raw.config };\n const positioned: NonNullable<PartMeasure[\"staffConfigs\"]>[number] = { config };\n if (raw.id !== undefined) positioned.id = raw.id;\n if (raw._c !== undefined) positioned._c = raw._c;\n if (raw._x !== undefined) positioned._x = raw._x;\n if (raw.position) positioned.position = parseRhythmicPosition(raw.position);\n if (raw.staff !== undefined) positioned.staff = raw.staff;\n return positioned;\n}\n", "// MNX Parser \u2014 parseContent (split from parser.ts)\n//\n// All schema-defined inputs (sequence, sequence-content, event, grace,\n// tuplet, multi-note-tremolo, space, note, kit-note, tie, pitch,\n// note-value, lyrics, slur, event-markings, rest, written, accidental-\n// display) now consume the generated Raw* types. Vendor-extension\n// blocks under `_x.viritura` (glissandos, slur shape overrides, trill,\n// ornaments, arpeggio direction, fingerings, staccatissimo wedge) keep\n// narrow VendorObj casts because the MNX schema deliberately leaves vendor\n// dicts opaque. Caesura is native as of MNX schema version 36; the legacy\n// `_x.viritura.caesura` vendor form is no longer read (MNX has not yet\n// declared a stable version, so pre-v36 documents are not supported).\n\nimport type {\n NoteEvent,\n Note,\n KitNote,\n Sequence,\n Duration,\n SequenceContent,\n Tie,\n Markings,\n BreathMarkSymbol,\n Fermata,\n FermataSymbol,\n FermataDuration,\n Placement,\n DirectionHint,\n UpDownAuto,\n UpDown,\n OrnamentType,\n Trill,\n Slur,\n Glissando,\n GlissandoStyle,\n ArpeggioDirection,\n CaesuraStyle,\n Lyrics,\n LyricLine,\n LyricLineType,\n Grace,\n GraceType,\n TupletDuration,\n Tuplet,\n TupletBracket,\n TupletDisplaySetting,\n TupletSpan,\n MultiNoteTremolo,\n Space,\n AccidentalDisplay,\n AccidentalEnclosureSymbol,\n Pitch,\n} from \"@viritura/core\";\nimport type { NoteValueBase, StemDirection, Step, Octave } from \"@viritura/core\";\n\nimport type {\n Sequence as RawSequence,\n SequenceContent as RawSequenceContentList,\n Event as RawEvent,\n Grace as RawGrace,\n Tuplet as RawTuplet,\n Space as RawSpace,\n MultiNoteTremolo as RawMultiNoteTremolo,\n Note as RawNote,\n KitNote as RawKitNote,\n Tie as RawTie,\n Pitch as RawPitch,\n NoteValue as RawNoteValue,\n Slur as RawSlur,\n Lyrics as RawLyrics,\n EventMarkings as RawEventMarkings,\n Rest as RawRest,\n} from \"@viritura/core/raw\";\nimport type {\n EventExtensions as RawEventExt,\n EventMarkingsExtensions as RawEventMarkingsExt,\n SlurExtensions as RawSlurExt,\n NoteExtensions as RawNoteExt,\n TupletExtensions as RawTupletExt,\n Glissando as RawGlissando,\n} from \"@viritura/core/raw-viritura\";\n\n/** A single sequence-content item: discriminated union from the schema. */\ntype RawSequenceItem = RawSequenceContentList[number];\n\n/** Shape of a tuplet's `inner`/`outer` (note-value-quantity). */\ntype RawNoteValueQuantity = RawTuplet[\"inner\"];\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sequences (voices)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseSequences(arr: RawSequence[] | undefined): Sequence[] {\n if (!arr) return [];\n return arr.map(parseSequence);\n}\n\nfunction parseSequence(raw: RawSequence): Sequence {\n const seq: Sequence = {\n content: parseContent(raw.content),\n };\n if (raw.fullMeasure) {\n const fm = raw.fullMeasure;\n seq.fullMeasure = {\n visualDuration: fm.visualDuration ? parseDuration(fm.visualDuration) : { base: \"whole\" },\n };\n if (fm.staffPosition !== undefined) {\n seq.fullMeasure.staffPosition = fm.staffPosition;\n }\n if (fm.fermata) seq.fullMeasure.fermata = { ...fm.fermata };\n }\n if (raw.staff !== undefined) seq.staff = raw.staff;\n if (raw.voice !== undefined) seq.voice = raw.voice;\n if (raw.directionHint !== undefined) seq.directionHint = raw.directionHint as DirectionHint;\n return seq;\n}\n\nfunction parseContent(arr: RawSequenceContentList | undefined): SequenceContent[] {\n if (!arr) return [];\n return arr.map(parseSequenceContent);\n}\n\nfunction parseSequenceContent(raw: RawSequenceItem): SequenceContent {\n // The discriminator is `type` \u2014 required on every variant except `event`,\n // where it's optional (default \"event\").\n const type = (raw as { type?: string }).type;\n switch (type) {\n case \"tuplet\":\n return parseTuplet(raw as RawTuplet);\n case \"tremolo\":\n return parseMultiNoteTremolo(raw as RawMultiNoteTremolo);\n case \"grace\":\n return parseGrace(raw as RawGrace);\n case \"space\":\n return parseSpace(raw as RawSpace);\n default:\n return parseEvent(raw as RawEvent);\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Tuplet\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseTupletDuration(raw: RawNoteValueQuantity): TupletDuration {\n return {\n duration: parseDuration(raw.duration),\n multiple: raw.multiple,\n };\n}\n\nfunction parseTuplet(raw: RawTuplet): Tuplet {\n const t: Tuplet = {\n type: \"tuplet\",\n inner: parseTupletDuration(raw.inner),\n outer: parseTupletDuration(raw.outer),\n content: parseContent(raw.content),\n };\n if (raw.bracket !== undefined) {\n t.bracket = raw.bracket as TupletBracket;\n }\n if (raw.showNumber !== undefined) {\n t.showNumber = raw.showNumber as TupletDisplaySetting;\n }\n if (raw.showValue !== undefined) {\n t.showValue = raw.showValue as TupletDisplaySetting;\n }\n if (raw.placement !== undefined) t.placement = raw.placement as Placement;\n const span = (raw._x?.[\"viritura\"] as RawTupletExt | undefined)?.span;\n if (span) t.span = { id: span.id, type: span.type as TupletSpan[\"type\"] };\n return t;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Multi-note tremolo\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseMultiNoteTremolo(raw: RawMultiNoteTremolo): MultiNoteTremolo {\n const tremolo: MultiNoteTremolo = {\n type: \"tremolo\",\n content: raw.content.map(parseEvent),\n marks: raw.marks,\n outer: parseTupletDuration(raw.outer),\n };\n if (raw.individualDuration !== undefined) tremolo.individualDuration = parseDuration(raw.individualDuration);\n return tremolo;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Grace notes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseGrace(raw: RawGrace): Grace {\n const grace: Grace = {\n type: \"grace\",\n content: raw.content.map(parseEvent),\n };\n if (raw.graceType !== undefined) grace.graceType = raw.graceType as GraceType;\n if (raw.slash !== undefined) grace.slash = raw.slash;\n if (raw.color) grace.color = raw.color;\n return grace;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Space\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseSpace(raw: RawSpace): Space {\n return {\n type: \"space\",\n duration: raw.duration as [number, number],\n };\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Events\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseEvent(raw: RawEvent): NoteEvent {\n const event: NoteEvent = {\n type: \"event\",\n duration: parseDuration(raw.duration),\n };\n\n if (raw.id) event.id = raw.id;\n\n if (typeof raw.staff === \"number\") event.staff = raw.staff;\n\n if (raw.rest !== undefined) {\n const restRaw: RawRest = raw.rest;\n const rest: { staffPosition?: number } = {};\n if (typeof restRaw.staffPosition === \"number\") {\n rest.staffPosition = restRaw.staffPosition;\n }\n event.rest = rest;\n }\n\n if (raw.notes) {\n event.notes = parseNotes(raw.notes);\n }\n\n if (raw.kitNotes) {\n event.kitNotes = raw.kitNotes.map(parseKitNote);\n }\n\n if (raw.slurs) {\n event.slurs = raw.slurs.map(parseSlur);\n }\n\n if (raw.markings) {\n event.markings = parseMarkings(raw.markings);\n }\n\n // Native MNX fermata (event-level since v15).\n if (raw.fermata !== undefined) {\n const f = raw.fermata;\n const fermata: Fermata = {};\n if (f.symbol) fermata.symbol = f.symbol as FermataSymbol;\n if (f.duration) fermata.duration = f.duration as FermataDuration;\n if (f.placement) fermata.placement = f.placement as Placement;\n if (f.pointing) fermata.pointing = f.pointing;\n event.fermata = fermata;\n }\n\n if (raw.lyrics) {\n event.lyrics = parseLyrics(raw.lyrics);\n }\n\n if (raw.stemDirection) {\n event.stemDirection = raw.stemDirection as StemDirection;\n }\n\n // _x.viritura vendor extensions on event\n const viritura = raw._x?.[\"viritura\"] as RawEventExt | undefined;\n if (viritura?.glissandos) {\n event.glissandos = viritura.glissandos.map(parseGlissando);\n }\n\n return event;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Markings\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// Helper: parse the `placement` field if present.\nfunction parsePlacement(o: { placement?: Placement } | undefined): { placement?: Placement } {\n if (o && o.placement) return { placement: o.placement };\n return {};\n}\n\n// Parse the standard MNX-spec markings on a marking container.\nfunction parseStandardMarkings(raw: RawEventMarkings, m: Markings): void {\n if (raw.staccato !== undefined) m.staccato = parsePlacement(raw.staccato);\n if (raw.staccatissimo !== undefined) m.staccatissimo = parsePlacement(raw.staccatissimo);\n if (raw.spiccato !== undefined) m.spiccato = parsePlacement(raw.spiccato);\n if (raw.tenuto !== undefined) m.tenuto = parsePlacement(raw.tenuto);\n if (raw.accent !== undefined) {\n // MNX spec: accent has only `placement` \u2014 no `pointing`.\n m.accent = parsePlacement(raw.accent);\n }\n if (raw.strongAccent !== undefined) {\n const sa = raw.strongAccent;\n m.strongAccent = parsePlacement(sa);\n if (sa.pointing) m.strongAccent.pointing = sa.pointing as UpDownAuto;\n }\n if (raw.tremolo !== undefined) {\n const t = raw.tremolo;\n m.tremolo = { marks: t.marks, ...parsePlacement(t) };\n }\n if (raw.softAccent !== undefined) m.softAccent = parsePlacement(raw.softAccent);\n if (raw.stress !== undefined) m.stress = parsePlacement(raw.stress);\n if (raw.unstress !== undefined) m.unstress = parsePlacement(raw.unstress);\n if (raw.breath !== undefined) {\n const b = raw.breath;\n m.breath = parsePlacement(b);\n if (b.symbol) m.breath.symbol = b.symbol as BreathMarkSymbol;\n }\n if (raw.caesura !== undefined) {\n m.caesura = {};\n if (raw.caesura.shape) m.caesura.style = raw.caesura.shape as CaesuraStyle;\n if (raw.caesura.marks) m.caesura.marks = raw.caesura.marks as 1 | 2;\n }\n if (raw.bowDirection !== undefined) {\n const bd = raw.bowDirection;\n m.bowDirection = {\n direction: bd.direction as UpDown,\n ...parsePlacement(bd),\n };\n }\n}\n\n// Parse the `_x.viritura` vendor extensions on a marking container.\n// NOTE: schema does NOT define `arpeggio` on event-markings-extensions, but\n// existing fixtures use it (legacy shape). Read it through a narrow cast.\nfunction parseVirituraMarkings(viritura: RawEventMarkingsExt, m: Markings): void {\n if (viritura.staccatissimoWedge !== undefined) {\n m.staccatissimoWedge = parsePlacement(viritura.staccatissimoWedge);\n }\n if (viritura.trill !== undefined) {\n const t = viritura.trill;\n const trill: Trill = {};\n if (t.accidental !== undefined) trill.accidental = t.accidental;\n if (t.showSymbol !== undefined) trill.showSymbol = t.showSymbol;\n if (t.extension !== undefined) {\n trill.extension = {\n target: t.extension.target,\n ...(t.extension.targetEdge !== undefined ? { targetEdge: t.extension.targetEdge } : {}),\n };\n }\n m.trill = trill;\n }\n if (viritura.ornaments !== undefined) {\n m.ornaments = viritura.ornaments.map((o) => o as OrnamentType);\n }\n const arpeggio = (viritura as { arpeggio?: { direction?: string } }).arpeggio;\n if (arpeggio !== undefined) {\n m.arpeggio = {};\n if (arpeggio.direction) m.arpeggio.direction = arpeggio.direction as ArpeggioDirection;\n }\n if (viritura.fingerings !== undefined) {\n m.fingerings = viritura.fingerings.map((f) => ({ finger: f.finger }));\n }\n}\n\nfunction parseMarkings(raw: RawEventMarkings): Markings {\n const m: Markings = {};\n parseStandardMarkings(raw, m);\n const viritura = raw._x?.[\"viritura\"] as RawEventMarkingsExt | undefined;\n if (viritura) parseVirituraMarkings(viritura, m);\n return m;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Lyrics\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseLyrics(raw: RawLyrics): Lyrics {\n const lyrics: Lyrics = {};\n if (raw.lines) {\n const linesObj = raw.lines;\n const lines: Record<string, LyricLine> = {};\n for (const key of Object.keys(linesObj)) {\n const lineRaw = linesObj[key]!;\n const line: LyricLine = { text: lineRaw.text };\n if (lineRaw.type !== undefined) line.type = lineRaw.type as LyricLineType;\n lines[key] = line;\n }\n lyrics.lines = lines;\n }\n return lyrics;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Slurs\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseSlur(raw: RawSlur): Slur {\n const slur: Slur = { target: raw.target };\n if (raw.side) slur.side = raw.side;\n if (raw.sideEnd) slur.sideEnd = raw.sideEnd;\n if (raw.lineType) slur.lineType = raw.lineType as \"solid\" | \"dashed\" | \"dotted\";\n if (raw.startNote) slur.startNote = raw.startNote;\n if (raw.endNote) slur.endNote = raw.endNote;\n // _x.viritura.shape vendor extension: per-handle bezier overrides for engrave mode.\n const viritura = raw._x?.[\"viritura\"] as RawSlurExt | undefined;\n const shapeObj = viritura?.shape;\n if (shapeObj) {\n const shape: NonNullable<Slur[\"shape\"]> = {};\n const readPair = (v: number[] | undefined): [number, number] | undefined => {\n if (Array.isArray(v) && v.length === 2 && typeof v[0] === \"number\" && typeof v[1] === \"number\") {\n return [v[0], v[1]];\n }\n return undefined;\n };\n const p0 = readPair(shapeObj.p0);\n if (p0) shape.p0 = p0;\n const p1 = readPair(shapeObj.p1);\n if (p1) shape.p1 = p1;\n const p2 = readPair(shapeObj.p2);\n if (p2) shape.p2 = p2;\n const p3 = readPair(shapeObj.p3);\n if (p3) shape.p3 = p3;\n if (shape.p0 || shape.p1 || shape.p2 || shape.p3) slur.shape = shape;\n }\n return slur;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Glissandos (vendor extension \u2014 no schema)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseGlissando(raw: RawGlissando): Glissando {\n const gliss: Glissando = { target: raw.target };\n if (raw.kind) gliss.kind = raw.kind;\n if (raw.style) gliss.style = raw.style as GlissandoStyle;\n if (raw.text) gliss.text = raw.text;\n if (raw.showText !== undefined) gliss.showText = raw.showText;\n return gliss;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Notes\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseNotes(arr: RawNote[]): Note[] {\n return arr.map(parseNote);\n}\n\nfunction parseNote(raw: RawNote): Note {\n const note: Note = {\n pitch: parsePitch(raw.pitch),\n };\n\n if (raw.id) note.id = raw.id;\n\n if (raw.ties) {\n note.ties = raw.ties.map(parseTie);\n }\n\n if (typeof raw.staff === \"number\") note.staff = raw.staff;\n\n if (raw.accidentalDisplay) {\n const ad = raw.accidentalDisplay;\n const display: AccidentalDisplay = { show: ad.show };\n if (ad.force !== undefined) display.force = ad.force;\n if (ad.enclosure) {\n display.enclosure = { symbol: ad.enclosure.symbol as AccidentalEnclosureSymbol };\n }\n note.accidentalDisplay = display;\n }\n\n if (raw.written) {\n note.written = {};\n if (raw.written.diatonicDelta !== undefined) note.written.diatonicDelta = raw.written.diatonicDelta;\n }\n\n // _x.viritura.notehead: per-note notehead-shape override (MNX issue #249).\n const viritura = raw._x?.[\"viritura\"] as RawNoteExt | undefined;\n if (viritura?.notehead) note.notehead = viritura.notehead;\n\n return note;\n}\n\nfunction parseKitNote(raw: RawKitNote): KitNote {\n const kn: KitNote = {\n kitComponent: raw.kitComponent,\n };\n if (raw.id) kn.id = raw.id;\n if (typeof raw.staff === \"number\") kn.staff = raw.staff;\n if (raw.ties) {\n kn.ties = raw.ties.map(parseTie);\n }\n if (raw.perform !== undefined) {\n const p = raw.perform;\n const perform: { id?: string } = {};\n if (typeof p.id === \"string\") perform.id = p.id;\n kn.perform = perform;\n }\n return kn;\n}\n\nfunction parseTie(raw: RawTie): Tie {\n const tie: Tie = {};\n if (raw.target) tie.target = raw.target;\n if (raw.targetType) tie.targetType = raw.targetType;\n if (raw.side) tie.side = raw.side;\n if (raw.lv !== undefined) tie.lv = raw.lv;\n return tie;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Pitch\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parsePitch(raw: RawPitch): Pitch {\n const pitch: Pitch = {\n step: (raw.step as Step) ?? \"C\",\n octave: (raw.octave as Octave) ?? 4,\n };\n if (raw.alter !== undefined) pitch.alter = raw.alter;\n return pitch;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration (note-value)\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nfunction parseDuration(raw: RawNoteValue): Duration {\n const dur: Duration = {\n base: (raw.base as NoteValueBase) ?? \"quarter\",\n };\n if (raw.dots !== undefined) dur.dots = raw.dots;\n return dur;\n}\n", "// MNX Parser \u2014 parseLayout (split from parser.ts)\n//\n// Consumes the generated Raw* wire types from @viritura/core/raw and produces\n// the decoded model from @viritura/core. Field access is now type-checked\n// against the MNX schema; schema drift surfaces as compile errors here.\n//\n// Vendor-extension access (`_x.viritura.*`) still requires casts because\n// the schema deliberately leaves vendor dicts untyped \u2014 see\n// docs/spec/viritura-extensions.md for the typed shapes we layer on top.\n\nimport type {\n LayoutDefinition,\n LayoutContent,\n LayoutGroup,\n LayoutStaff,\n LayoutSource,\n ScoreDefinition,\n PageDefinition,\n SystemDefinition,\n LayoutChange,\n MultimeasureRestRange,\n PageSetup,\n PageTurnSettings,\n InstrumentNameDisplaySettings,\n} from \"@viritura/core\";\n\nimport type {\n SystemLayout as RawSystemLayout,\n StaffGroup as RawStaffGroup,\n Staff as RawStaff,\n StaffSource as RawStaffSource,\n Score as RawScoreDef,\n Page as RawPage,\n System as RawSystem,\n LayoutChange as RawLayoutChange,\n MultimeasureRest as RawMultimeasureRest,\n} from \"@viritura/core/raw\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MNX Layout definitions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport function parseLayoutDefinition(raw: RawSystemLayout): LayoutDefinition {\n const ld: LayoutDefinition = {\n // `id` is optional in the schema (every global-attrs has optional id);\n // assignMissingIds() later fills any gaps. Preserve undefined here.\n id: raw.id as string,\n content: raw.content.map(parseLayoutContent),\n };\n\n // Vendor extension: _x.viritura.derived \u2014 flags auto-derived layouts\n // (e.g. hide-staff prunes) so the GC can distinguish them from\n // user-authored layouts.\n const viritura = raw._x?.[\"viritura\"] as Record<string, unknown> | undefined;\n if (viritura?.[\"derived\"] === true) {\n ld._x = { viritura: { derived: true } };\n }\n\n return ld;\n}\n\nfunction parseLayoutContent(raw: RawStaffGroup | RawStaff): LayoutContent {\n if (raw.type === \"group\") return parseLayoutGroup(raw);\n return parseLayoutStaff(raw);\n}\n\nfunction parseLayoutGroup(raw: RawStaffGroup): LayoutGroup {\n const group: LayoutGroup = {\n type: \"group\",\n content: raw.content.map(parseLayoutContent),\n };\n if (raw.symbol) group.symbol = raw.symbol;\n if (raw.label) group.label = raw.label;\n if (raw.barlineStyle) group.barlineStyle = raw.barlineStyle;\n return group;\n}\n\nfunction parseLayoutStaff(raw: RawStaff): LayoutStaff {\n const staff: LayoutStaff = {\n type: \"staff\",\n sources: (raw.sources ?? []).map(parseLayoutSource),\n };\n if (raw.label) staff.label = raw.label;\n if (raw.labelref) staff.labelref = raw.labelref;\n return staff;\n}\n\nfunction parseLayoutSource(raw: RawStaffSource): LayoutSource {\n const src: LayoutSource = {\n part: raw.part,\n };\n if (raw.staff !== undefined) src.staff = raw.staff;\n if (raw.stem) src.stem = raw.stem;\n if (raw.voice) src.voice = raw.voice;\n if (raw.labelref) src.labelref = raw.labelref;\n return src;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// MNX Score definitions\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n// eslint-disable-next-line complexity -- MNX score definitions flatten several independent optional vendor fields\nexport function parseScoreDefinition(raw: RawScoreDef): ScoreDefinition {\n const sd: ScoreDefinition = {};\n if (raw.name) sd.name = raw.name;\n if (raw.layout) sd.layout = raw.layout;\n if (raw.multimeasureRests && raw.multimeasureRests.length > 0) {\n sd.multimeasureRests = raw.multimeasureRests.map((m: RawMultimeasureRest): MultimeasureRestRange => ({\n start: m.start,\n duration: m.duration,\n }));\n }\n if (raw.pages && raw.pages.length > 0) {\n sd.pages = raw.pages.map(parsePageDefinition);\n }\n if (raw.useWritten !== undefined) sd.useWritten = raw.useWritten;\n\n // Vendor extension: _x.viritura.pageSetup\n // (vendor dicts are untyped in the schema; narrow-cast the known shape.)\n const viritura = raw._x?.[\"viritura\"] as Record<string, unknown> | undefined;\n const labelsRaw = viritura?.[\"instrumentNameDisplay\"] as Record<string, unknown> | undefined;\n if (labelsRaw) {\n sd.instrumentNameDisplay = {\n firstSystem: labelsRaw[\"firstSystem\"] as InstrumentNameDisplaySettings[\"firstSystem\"],\n subsequentSystems: labelsRaw[\"subsequentSystems\"] as InstrumentNameDisplaySettings[\"subsequentSystems\"],\n };\n }\n const layoutBreaksRaw = viritura?.[\"layoutBreaks\"];\n if (Array.isArray(layoutBreaksRaw)) {\n sd.layoutBreaks = layoutBreaksRaw.flatMap((value) => {\n if (!value || typeof value !== \"object\") return [];\n const entry = value as Record<string, unknown>;\n if (typeof entry[\"measure\"] !== \"string\") return [];\n if (entry[\"kind\"] !== \"system\" && entry[\"kind\"] !== \"page\") return [];\n return [{ measure: entry[\"measure\"], kind: entry[\"kind\"] }];\n });\n }\n const psRaw = viritura?.[\"pageSetup\"] as Record<string, unknown> | undefined;\n if (psRaw) {\n const pageSetup: Partial<PageSetup> = {};\n if (psRaw[\"width\"] !== undefined) pageSetup.width = psRaw[\"width\"] as number;\n if (psRaw[\"height\"] !== undefined) pageSetup.height = psRaw[\"height\"] as number;\n if (psRaw[\"orientation\"]) pageSetup.orientation = psRaw[\"orientation\"] as \"portrait\" | \"landscape\";\n if (psRaw[\"margins\"]) {\n const m = psRaw[\"margins\"] as Record<string, unknown>;\n pageSetup.margins = {\n top: (m[\"top\"] as number) ?? 15,\n right: (m[\"right\"] as number) ?? 10,\n bottom: (m[\"bottom\"] as number) ?? 15,\n left: (m[\"left\"] as number) ?? 15,\n };\n }\n if (psRaw[\"spatiumMm\"] !== undefined) pageSetup.spatiumMm = psRaw[\"spatiumMm\"] as number;\n const ptRaw = psRaw[\"pageTurns\"] as Record<string, unknown> | undefined;\n if (ptRaw && ptRaw[\"enabled\"] !== undefined) {\n const weightsRaw = ptRaw[\"weights\"] as Record<string, unknown> | undefined;\n const pageTurns: PageTurnSettings = {\n enabled: ptRaw[\"enabled\"] as boolean,\n ...(ptRaw[\"preset\"] ? { preset: ptRaw[\"preset\"] as \"relaxed\" | \"professional\" } : {}),\n ...(ptRaw[\"comfortableSecs\"] !== undefined ? { comfortableSecs: ptRaw[\"comfortableSecs\"] as number } : {}),\n ...(ptRaw[\"vsSecs\"] !== undefined ? { vsSecs: ptRaw[\"vsSecs\"] as number } : {}),\n ...(ptRaw[\"minAcceptableSecs\"] !== undefined\n ? { minAcceptableSecs: ptRaw[\"minAcceptableSecs\"] as number }\n : {}),\n ...(ptRaw[\"targetFillFraction\"] !== undefined\n ? { targetFillFraction: ptRaw[\"targetFillFraction\"] as number }\n : {}),\n ...(ptRaw[\"minFillFraction\"] !== undefined ? { minFillFraction: ptRaw[\"minFillFraction\"] as number } : {}),\n ...(ptRaw[\"verticalJustifyThreshold\"] !== undefined\n ? { verticalJustifyThreshold: ptRaw[\"verticalJustifyThreshold\"] as number }\n : {}),\n ...(ptRaw[\"allowPartialPages\"] !== undefined\n ? { allowPartialPages: ptRaw[\"allowPartialPages\"] as boolean }\n : {}),\n ...(ptRaw[\"allowIntentionalBlanks\"] !== undefined\n ? { allowIntentionalBlanks: ptRaw[\"allowIntentionalBlanks\"] as boolean }\n : {}),\n ...(ptRaw[\"titlePage\"] ? { titlePage: ptRaw[\"titlePage\"] as PageTurnSettings[\"titlePage\"] } : {}),\n ...(ptRaw[\"firstPageRecto\"] !== undefined ? { firstPageRecto: ptRaw[\"firstPageRecto\"] as boolean } : {}),\n ...(ptRaw[\"emitVsMarks\"] !== undefined ? { emitVsMarks: ptRaw[\"emitVsMarks\"] as boolean } : {}),\n ...(ptRaw[\"defaultBpm\"] !== undefined ? { defaultBpm: ptRaw[\"defaultBpm\"] as number } : {}),\n };\n if (weightsRaw) {\n pageTurns.weights = {\n ...(weightsRaw[\"density\"] !== undefined ? { density: weightsRaw[\"density\"] as number } : {}),\n ...(weightsRaw[\"turn\"] !== undefined ? { turn: weightsRaw[\"turn\"] as number } : {}),\n ...(weightsRaw[\"sparse\"] !== undefined ? { sparse: weightsRaw[\"sparse\"] as number } : {}),\n ...(weightsRaw[\"titlePage\"] !== undefined ? { titlePage: weightsRaw[\"titlePage\"] as number } : {}),\n ...(weightsRaw[\"blankPage\"] !== undefined ? { blankPage: weightsRaw[\"blankPage\"] as number } : {}),\n ...(weightsRaw[\"timeMarking\"] !== undefined ? { timeMarking: weightsRaw[\"timeMarking\"] as number } : {}),\n };\n }\n pageSetup.pageTurns = pageTurns;\n }\n if (Object.keys(pageSetup).length > 0) {\n sd.pageSetup = pageSetup as PageSetup;\n }\n }\n\n return sd;\n}\n\nfunction parsePageDefinition(raw: RawPage): PageDefinition {\n return {\n systems: (raw.systems ?? []).map(parseSystemDefinition),\n };\n}\n\nfunction parseSystemDefinition(raw: RawSystem): SystemDefinition {\n const sd: SystemDefinition = {\n measure: raw.measure,\n };\n if (raw.layout) sd.layout = raw.layout;\n if (raw.layoutChanges && raw.layoutChanges.length > 0) {\n sd.layoutChanges = raw.layoutChanges.map((lc: RawLayoutChange): LayoutChange => ({\n layout: lc.layout,\n location: {\n measure: lc.location.measure,\n ...(lc.location.position\n ? {\n position: {\n // Schema types fraction as integer-unsigned[]; the decoded\n // model uses a [num, den] tuple. Narrow array\u2192tuple here.\n fraction: lc.location.position.fraction as [number, number],\n },\n }\n : {}),\n },\n }));\n }\n return sd;\n}\n", "/**\n * Re-export of @viritura/core diagnostics. Kept here to avoid breaking the\n * `./mnx/diagnostics` import path used by the parser; new code should import\n * from `@viritura/core` (or via `@viritura/format`'s top-level re-export).\n */\nexport { DiagnosticCollector, ptr, ptrChild, type MnxDiagnostic, type DiagnosticSeverity } from \"@viritura/core\";\n", "/**\n * @viritura/midi \u2014 MIDI event timeline and Web MIDI API output.\n *\n * Converts Viritura score models into MIDI event timelines for playback\n * scheduling, and provides Web MIDI API integration for external synths.\n */\n\n// MIDI types\nexport type { MidiTimeline, MidiEvent, TempoMapEntry, TimelineDiagnostic } from \"./types\";\n\n// Timeline generator\nexport { generateTimeline, durationBeats, expandMeasureOrder } from \"./timeline\";\nexport { expandMeasureRepeats, expandScoreMeasureRepeats } from \"./measureRepeats\";\nexport type { TimelineOptions } from \"./timeline\";\nexport { getChordPlaybackPart } from \"./chordPlayback\";\nexport type { ChordPlaybackPart } from \"./chordPlayback\";\n\n// Notation-level performance events for VST articulation mapping\nexport { generatePerformanceEvents } from \"./performanceEvents\";\nexport type { PerformanceEvent, PerformanceNote, Articulations, PlayingState } from \"./performanceEvents\";\n\n// Tempo map\nexport { buildTempoMap, measureBeatsFromTime, tempoNoteBeats, effectiveQpm, spqAtTime, DEFAULT_BPM } from \"./tempoMap\";\n\n// Dynamics\nexport { applyArticulationVelocity, articulationDurationScale, metricAccentOffset, velocityHumanize } from \"./dynamics\";\n\n// Coupled dynamics envelope (velocity \u2194 CC11)\nexport {\n DYNAMIC_AXES,\n DEFAULT_DYNAMIC,\n dynamicToAxes,\n buildDynamicsEnvelope,\n sampleDynamics,\n noteVelocityAt,\n hasAnyDynamics,\n cc11Events,\n} from \"./dynamicsEnvelope\";\nexport type { DynamicAxes, DynamicsEnvelope, Cc11Event } from \"./dynamicsEnvelope\";\nexport {\n compileDynamicProgram,\n laneForScope,\n laneForSequence,\n playbackLaneId,\n realizeDynamicsEnvelope,\n selectDynamicResponseProfile,\n} from \"./dynamicPlayback\";\nexport type {\n DynamicPlaybackDiagnostic,\n DynamicProgram,\n DynamicResponseProfile,\n PlaybackLane,\n PlaybackLaneId,\n} from \"./dynamicPlayback\";\n\n// Web MIDI API\nexport { isWebMidiSupported, listMidiInputs, listMidiOutputs, queryMidiPermission } from \"./webMidi\";\nexport { MidiOutputManager } from \"./MidiOutputManager\";\nexport type { MidiOutputPort, MidiOutputManagerEvents } from \"./MidiOutputManager\";\nexport { MidiInputManager, decodeMidiInputMessage } from \"./midiInput\";\nexport type { MidiInputManagerEvents, MidiInputMessage, MidiInputMessageKind, MidiInputPort } from \"./midiInput\";\nexport {\n controllerPortRole,\n controllerProfileSourceUrl,\n findControllerProfile,\n listControllerProfiles,\n parseControllerProfile,\n resolveControllerAction,\n isControllerPreviewMessage,\n} from \"./controllerProfiles\";\nexport type {\n ControllerAction,\n ControllerBinding,\n ControllerPortProfile,\n ControllerProfile,\n} from \"./controllerProfiles\";\n", "/**\n * MIDI timeline generator \u2014 converts a Score into a time-ordered sequence\n * of MIDI noteOn/noteOff events with absolute timestamps.\n *\n * The generator is stateless and deterministic: the same Score always\n * produces the same MidiTimeline.\n *\n * Algorithm:\n * 1. Expand repeats and jumps into a linear measure order\n * 2. Build a tempo map from the expanded global measures\n * 3. For each part, walk the expanded measures and emit MIDI events\n * 4. Resolve ties (suppress intermediate noteOn/noteOff pairs)\n * 5. Sort all events by time\n */\n\n/* eslint-disable max-lines -- The ornament-expansion cluster (kit roll,\n single/multi-note tremolo, trill, grace, tuplet) is mutually recursive with\n processNoteEvent/processContentItem and shares a dozen module-private\n helpers (eventTime, eventSeconds, timingHumanize, pitchToMidi, \u2026). Splitting\n it out would require exporting those internals and create circular imports \u2014\n more risk than the ~10-line overage warrants. Revisit if the timing/event\n helpers are first promoted to their own sibling. */\n\nimport type {\n Score,\n GlobalMeasure,\n NoteEvent,\n Sequence,\n SequenceContent,\n Duration,\n TimeSignature,\n Grace,\n Note,\n KitNote,\n Pitch,\n Step,\n Octave,\n} from \"@viritura/core\";\nimport { pitchToMidi, isRest, DURATION_BEATS, SHARP_ORDER, FLAT_ORDER } from \"@viritura/core\";\nimport { detectToCodaMeasureIndex, expandMeasureOrder } from \"./repeatExpansion\";\nimport { expandScoreMeasureRepeats } from \"./measureRepeats\";\nimport { compileChordPlayback, chordMidiEvents } from \"./chordPlayback\";\nimport { analyzeImpliedSectionDynamics, type ImpliedSectionDynamicAnchor } from \"./sectionDynamics\";\nimport { buildHoldSchedule, type HoldSchedule, type MeasureHold } from \"./holds\";\nimport { playbackGlobalMeasures, suppressCadenzaFermataHolds } from \"./cadenzaTiming\";\nimport { collectPartLegatoOut, flushVoiceLegato, flushAllLegato, pushVoiceLegato, resolveVoiceLegato } from \"./legato\";\nimport { ARCO_CAPABLE_PROGRAMS, muteFamilyForProgram, applyMeasureTechniques, type TechniqueState } from \"./technique\";\nimport { resolveActiveTime, resolvePartStaffMeterTable, staffMeterRatioAt } from \"./staffMeterTiming\";\nexport { expandMeasureOrder };\n\n/** GM percussion channel (0-based; channel 10 in 1-based MIDI numbering). */\nexport const GM_DRUM_CHANNEL = 9;\n\n/**\n * Resolve the effective MIDI note for a pitched note.\n * For pitched notes, returns pitchToMidi(note.pitch).\n */\nfunction noteToMidi(note: Note): number {\n return pitchToMidi(note.pitch);\n}\n\n/** Resolve the effective MIDI note for a kit-note via the kitMidiMap. */\nfunction kitNoteToMidi(kn: KitNote, kitMidiMap: ReadonlyMap<string, number>): number {\n return kitMidiMap.get(kn.kitComponent) ?? -1;\n}\nimport type { MidiEvent, MidiTimeline, TempoMapEntry, TimelineDiagnostic } from \"./types\";\nimport { buildTempoMap, buildTempoModel } from \"./tempoMap\";\nimport { TempoModel } from \"./tempoModel\";\nimport {\n applyArticulationVelocity,\n articulationDurationScale,\n metricAccentOffset,\n velocityHumanize,\n timingHumanize,\n} from \"./dynamics\";\nimport { noteVelocityAt, emitDynamicsCc11, type DynamicsEnvelope } from \"./dynamicsEnvelope\";\nimport { compileDynamicProgram, laneForScope, laneForSequence } from \"./dynamicPlayback\";\nimport type { DynamicProgram } from \"./dynamicPlayback\";\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Constants\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Duration per grace note in beats (\u2248 32nd note). */\nconst GRACE_NOTE_BEATS = 0.125;\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Duration helpers\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Compute quarter-note beats for a Duration (base + dots). */\nexport function durationBeats(d: Duration): number {\n const base = DURATION_BEATS[d.base] ?? 1;\n if (!d.dots) return base;\n return base * (2 - Math.pow(2, -d.dots));\n}\n\n/** Convert a measure-relative fraction (of a whole note) to quarter-note beats. */\nexport function fractionToBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Main timeline generator\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Options for timeline generation. */\nexport interface TimelineOptions {\n /** GM program number per part index (used for tremolo sound selection). */\n partPrograms?: number[];\n /** Include the global derived Chords stream (default true). Full-score and\n * current-part extracts include it. Explicit selected-instrument callers\n * must pass false unless the entire visible score is selected. */\n includeGlobalChords?: boolean;\n /** Expand playback repeats and jumps (default true); false plays measures once in score order. */\n expandRepeats?: boolean;\n}\n\n/**\n * Per-part shared context threaded through every process* helper.\n * Pulled out so the process* signatures don't carry 13\u201315 parameters each.\n *\n * `partIndex`/`channel`/`gmProgram`/`timeSig`/`kitMidiMap`/`tieTargets`/\n * `pendingTieOffs`/`out`/`tempoMap` are invariant for the lifetime of a\n * single part's traversal; per-event state (measureStartTime, beatOffset,\n * baseVelocity, tupletRatio) stays as explicit parameters because it\n * mutates per recursion step.\n */\nexport interface PartCtx {\n readonly tempoMap: readonly TempoMapEntry[];\n /** Continuous tempo map (single source of timing). Event times are\n * `model.timeAtBeat(measureStartBeat + beatOffset)`, resolved at sub-bar\n * granularity so mid-bar tempo changes, rit./accel. and fermata/caesura time\n * insertions all take effect at any nesting depth (no per-bar quantization). */\n readonly model: TempoModel;\n /** Global quarter-note beat at the current measure's start (the hold-free\n * beat axis the model integrates over). */\n readonly measureStartBeat: number;\n readonly partIndex: number;\n readonly channel: number;\n readonly tieTargets: ReadonlySet<string>;\n readonly pendingTieOffs: Map<string, MidiEvent>;\n /** Event ids interior to a slur span; their noteOff is deferred and resolved as a legato overlap. */\n readonly legatoOutIds: ReadonlySet<string>;\n /** Deferred legato noteOffs awaiting the next note's onset, bucketed BY VOICE\n * so a slur in one voice isn't resolved by a note in another. Each off carries\n * its natural (fallback) time, overwritten on resolution. Threaded across\n * measures (like `pendingTieOffs`) so slurs cross bar lines. */\n readonly pendingLegato: Map<string, MidiEvent[]>;\n /** Identity of the voice (sequence) currently being processed. Tie and legato\n * pairing state is keyed by this so a multi-voice part (e.g. piano, two hands\n * sharing pitches) doesn't cross-contaminate noteOn/noteOff pairing. Mutated\n * per sequence in processPart. */\n voiceKey: string;\n readonly out: MidiEvent[];\n readonly gmProgram: number;\n /** Meter driving beat semantics (metric accent) for the sequence currently\n * being processed: the global measure's time signature, or a staff-local\n * meter's own time signature when the current sequence's staff carries an\n * effective `_x.viritura.staffMeters` declaration. Mutated per sequence in\n * `processPart`, mirroring `sequenceStaff`. */\n timeSig: TimeSignature;\n /** Exact ratio scaling this staff's written beats into global-measure-\n * equivalent beats for absolute playback timing (`1` for an ordinary staff\n * or a `sharedDuration` staff-local meter; the derived mapping ratio for\n * `fitMeasure`). Applied in `eventTime`/`eventSeconds` so a `fitMeasure`\n * staff's notes land at the same wall-clock position other staves'\n * corresponding beats do, even though its written durations differ.\n * Mutated per sequence, alongside `timeSig`. */\n staffMeterRatio: number;\n /** Active key signature (circle-of-fifths) at this measure; drives trill auxiliaries. */\n readonly keyFifths: number;\n readonly kitMidiMap: ReadonlyMap<string, number> | undefined;\n /** kit-component-id \u2192 GS drum-kit program override (bank 128). Hits on these\n * components play on a separate drum channel loaded with the given kit, so a\n * percussion staff can borrow a sound its main kit lacks (e.g. a Tam-tam from\n * the Ethnic kit). undefined when no component overrides its kit. */\n readonly kitAltProgramMap: ReadonlyMap<string, number> | undefined;\n /** Coupled velocity/CC11 dynamics over time for this part. Velocity is sampled\n * per note onset; CC11 is emitted separately (see emitDynamicsCc11). */\n dynamicsEnvelope: DynamicsEnvelope;\n playbackLaneId: string;\n readonly dynamicProgram: DynamicProgram;\n sequenceStaff: number;\n sequenceVoice: string;\n /** Seconds per quarter-note beat at this measure's start (for fermata resume). */\n readonly measureSpq: number;\n /** Fermata groups in this measure (from the hold schedule). The hold's time\n * insertion is baked into the tempo model at `insertBeat`, so a carrier whose\n * onset falls in [startBeat, spanEndBeat) rings through the gap automatically\n * (its model duration already spans the insertion); we only force-extend a\n * member that ends BEFORE `insertBeat` up to the group's resume time. Beats\n * are measure-relative. Empty when the measure has no fermata. */\n readonly fermataGroups: readonly { startBeat: number; spanEndBeat: number; insertBeat: number }[];\n}\n\n/** An empty timeline (no measures / no playable content). */\nfunction emptyTimeline(): MidiTimeline {\n return {\n events: [],\n duration: 0,\n totalBeats: 0,\n tempoMap: [],\n measureStartTimes: [],\n model: TempoModel.build([]),\n measureStartBeats: [],\n expandedMeasureToOriginal: [],\n measureTimeSignatures: [],\n diagnostics: [],\n };\n}\n\n/**\n * Generate a MIDI timeline from a Score.\n *\n * @param score - The score to convert\n * @param options - Optional generation options\n * @returns A MidiTimeline with time-sorted events, total duration, and tempo map\n */\nexport function generateTimeline(inputScore: Score, options?: TimelineOptions): MidiTimeline {\n // Simile marks stand in for earlier bars; substitute the real music before\n // anything downstream reads part measures.\n const score = expandScoreMeasureRepeats(inputScore);\n const impliedSectionDynamics = analyzeImpliedSectionDynamics(score);\n const globalMeasures = score.global.measures;\n if (globalMeasures.length === 0) {\n return emptyTimeline();\n }\n\n const chords = compileChordPlayback(score, options?.includeGlobalChords);\n // Step 1: Expand repeats/jumps into linear measure order\n const toCodaMeasureIndex = detectToCodaMeasureIndex(score);\n const measureOrder =\n options?.expandRepeats === false\n ? globalMeasures.map((_, index) => index)\n : expandMeasureOrder(globalMeasures, { toCodaMeasureIndex });\n if (measureOrder.length === 0) {\n return emptyTimeline();\n }\n\n // Step 2: Build the fermata/caesura hold schedule, then the tempo map. The\n // schedule shifts every later measure's start time so holds keep all parts\n // aligned through the pause.\n const expandedGlobal = playbackGlobalMeasures(score, measureOrder);\n const holdSchedule = suppressCadenzaFermataHolds(\n expandedGlobal,\n buildHoldSchedule(score, measureOrder, globalMeasures),\n );\n // Continuous tempo model + global beat axis (single source of event timing).\n // `measureStartTimes` is derived FROM the model so it matches event timing\n // through sub-bar tempo changes / holds. The legacy `tempoMap` entry array is\n // kept for UI, dynamics, and the score-engine consumers.\n const { tempoMap } = buildTempoMap(expandedGlobal, holdSchedule);\n const tempoBuild = buildTempoModel(expandedGlobal, holdSchedule);\n const { model, measureStartTimes, measureStartBeats } = tempoBuild;\n const timing: TimelineTiming = { tempoMap, measureStartTimes, model, measureStartBeats };\n\n // Step 3: For each part, generate MIDI events\n const allEvents: MidiEvent[] = [];\n const partChannels = allocatePartChannels(score.parts);\n\n for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {\n processPart(\n score,\n partIdx,\n options,\n partChannels,\n timing,\n measureOrder,\n globalMeasures,\n holdSchedule,\n impliedSectionDynamics.get(partIdx) ?? [],\n allEvents,\n );\n }\n\n if (chords) {\n for (const event of chordMidiEvents(chords, measureOrder, tempoBuild)) allEvents.push(event);\n }\n const diagnostics = assignPhysicalMidiChannels(allEvents, score);\n\n // Step 4: Sort by time, then noteOff before programChange before noteOn at same time\n const EVENT_TYPE_ORDER: Record<string, number> = { noteOff: 0, programChange: 1, controlChange: 1, noteOn: 2 };\n allEvents.sort((a, b) => {\n if (Math.abs(a.time - b.time) > 1e-9) return a.time - b.time;\n const aOrder = EVENT_TYPE_ORDER[a.type] ?? 1;\n const bOrder = EVENT_TYPE_ORDER[b.type] ?? 1;\n if (aOrder !== bOrder) return aOrder - bOrder;\n return a.midiNote - b.midiNote;\n });\n\n const duration = model.timeAtBeat(tempoBuild.totalBeats);\n\n // Active time signature per expanded measure (for the metronome click grid).\n const measureTimeSignatures = measureOrder.map((origIdx) => {\n const ts = resolveActiveTime(globalMeasures, origIdx);\n return { count: ts.count, unit: ts.unit };\n });\n\n return {\n events: allEvents,\n duration,\n tempoMap,\n measureStartTimes,\n model,\n totalBeats: tempoBuild.totalBeats,\n measureStartBeats,\n expandedMeasureToOriginal: measureOrder,\n measureTimeSignatures,\n diagnostics,\n };\n}\n\n/** Assign one MIDI 1 channel per independent dynamic lane where possible.\n * Internal SF2 playback ignores this projection and allocates unbounded synth\n * instances; this mapping is for Web MIDI and future SMF export. */\nfunction assignPhysicalMidiChannels(events: MidiEvent[], score: Score): TimelineDiagnostic[] {\n const melodicChannels = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15];\n const lanesByPart = new Map<number, Set<string>>();\n for (const event of events) {\n const laneId = event.playbackLaneId ?? `part:${event.partIndex}`;\n const lanes = lanesByPart.get(event.partIndex) ?? new Set<string>();\n lanes.add(laneId);\n lanesByPart.set(event.partIndex, lanes);\n }\n\n const channelByLane = new Map<string, number>();\n const diagnostics: TimelineDiagnostic[] = [];\n let melodicIndex = 0;\n for (const [partIndex, laneSet] of [...lanesByPart].sort(([left], [right]) => left - right)) {\n const laneIds = [...laneSet].sort();\n const percussion = !!score.parts[partIndex]?.kit && Object.keys(score.parts[partIndex]!.kit!).length > 0;\n if (percussion) {\n for (const laneId of laneIds) channelByLane.set(laneId, GM_DRUM_CHANNEL);\n if (laneIds.length > 1) {\n diagnostics.push({\n code: \"percussion-lane-collapse\",\n message: `Physical MIDI collapses ${laneIds.length} scoped percussion lanes in part ${partIndex} onto channel 10.`,\n playbackLaneIds: laneIds,\n });\n }\n continue;\n }\n for (const laneId of laneIds) {\n if (melodicIndex < melodicChannels.length) {\n channelByLane.set(laneId, melodicChannels[melodicIndex++]!);\n } else {\n channelByLane.set(laneId, melodicChannels[melodicIndex++ % melodicChannels.length]!);\n diagnostics.push({\n code: \"midi-channel-capacity\",\n message: `Physical MIDI channel capacity exceeded; lane ${laneId} must share a channel.`,\n playbackLaneIds: [laneId],\n });\n }\n }\n }\n\n for (const event of events) {\n event.channel = channelByLane.get(event.playbackLaneId ?? `part:${event.partIndex}`) ?? event.channel;\n }\n return diagnostics;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Tie target collection\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Add every tie-target note id from `notes` to `targets`. */\nfunction collectNoteTieTargets(notes: readonly Note[] | undefined, targets: Set<string>): void {\n if (!notes) return;\n for (const note of notes) {\n if (!note.ties) continue;\n for (const tie of note.ties) {\n if (tie.target) targets.add(tie.target);\n }\n }\n}\n\n/** Recursively collect all note IDs that are tie targets. */\nfunction collectTieTargets(content: readonly SequenceContent[], targets: Set<string>): void {\n for (const item of content) {\n if (item.type === \"event\") {\n collectNoteTieTargets(item.notes, targets);\n } else if (item.type === \"tuplet\") {\n collectTieTargets(item.content, targets);\n } else if (item.type === \"grace\") {\n // Grace notes may also have ties\n for (const evt of item.content) {\n collectNoteTieTargets(evt.notes, targets);\n }\n }\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sequence processing\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Allocate a MIDI channel per part, sending percussion parts to\n * `GM_DRUM_CHANNEL` and round-robining the rest across the remaining\n * 15 channels while skipping channel 9 (reserved for GM drums).\n */\nfunction allocatePartChannels(parts: readonly Score[\"parts\"][number][]): number[] {\n const MIDI_CHANNELS = 16;\n const channels: number[] = [];\n let melodicCounter = 0;\n for (const part of parts) {\n const isPerc = !!part.kit && Object.keys(part.kit).length > 0;\n if (isPerc) {\n channels.push(GM_DRUM_CHANNEL);\n } else {\n while (melodicCounter % MIDI_CHANNELS === GM_DRUM_CHANNEL) melodicCounter++;\n channels.push(melodicCounter % MIDI_CHANNELS);\n melodicCounter++;\n }\n }\n return channels;\n}\n\n/**\n * Build the `kit-component-id \u2192 MIDI note` map for a percussion part.\n * Returns undefined for non-percussion parts.\n */\nfunction buildKitMidiMap(score: Score, part: Score[\"parts\"][number]): ReadonlyMap<string, number> | undefined {\n if (!part.kit || Object.keys(part.kit).length === 0) return undefined;\n const map = new Map<string, number>();\n const sounds = score.global.sounds ?? {};\n for (const [id, comp] of Object.entries(part.kit)) {\n const sound = comp.sound ? sounds[comp.sound] : undefined;\n if (sound && typeof sound.midiNumber === \"number\") {\n map.set(id, sound.midiNumber);\n }\n }\n return map;\n}\n\n/**\n * Build the `kit-component-id \u2192 drum-kit program` override map. Only components\n * carrying a `drumKit` vendor field appear; their hits play on a dedicated drum\n * channel loaded with that GS kit program (e.g. a Tam-tam borrowed from the\n * Ethnic kit). Returns undefined when no component overrides its kit.\n */\nfunction buildKitAltProgramMap(part: Score[\"parts\"][number]): ReadonlyMap<string, number> | undefined {\n if (!part.kit) return undefined;\n let map: Map<string, number> | undefined;\n for (const [id, comp] of Object.entries(part.kit)) {\n if (typeof comp.drumKit === \"number\") {\n (map ??= new Map()).set(id, comp.drumKit);\n }\n }\n return map;\n}\n\n/**\n * Pre-scan a part to collect every note id that is the target of a tie.\n * These notes have their noteOn suppressed and instead extend the\n * preceding pending noteOff.\n */\nfunction collectPartTieTargets(part: Score[\"parts\"][number], measureOrder: readonly number[]): Set<string> {\n const tieTargets = new Set<string>();\n for (const expandedIdx of measureOrder) {\n const partMeasure = part.measures[expandedIdx];\n if (!partMeasure) continue;\n for (const seq of partMeasure.sequences) {\n collectTieTargets(seq.content, tieTargets);\n }\n }\n return tieTargets;\n}\n\n/** Timing bundle threaded into `processPart` (keeps the param count in check). */\ninterface TimelineTiming {\n readonly tempoMap: readonly TempoMapEntry[];\n readonly measureStartTimes: readonly number[];\n readonly model: TempoModel;\n readonly measureStartBeats: readonly number[];\n}\n\n/**\n * Generate every MIDI event for a single part, appending to `allEvents`.\n * Extracted from generateTimeline so neither function exceeds the\n * complexity / statement budget.\n */\nfunction processPart(\n score: Score,\n partIdx: number,\n options: TimelineOptions | undefined,\n partChannels: readonly number[],\n timing: TimelineTiming,\n measureOrder: readonly number[],\n globalMeasures: readonly GlobalMeasure[],\n holdSchedule: HoldSchedule,\n impliedDynamics: readonly ImpliedSectionDynamicAnchor[],\n allEvents: MidiEvent[],\n): void {\n const { tempoMap, measureStartTimes, model, measureStartBeats } = timing;\n const part = score.parts[partIdx]!;\n const channel = partChannels[partIdx]!;\n const gmProgram = options?.partPrograms?.[partIdx] ?? -1;\n const kitMidiMap = buildKitMidiMap(score, part);\n const kitAltProgramMap = buildKitAltProgramMap(part);\n const tieTargets = collectPartTieTargets(part, measureOrder);\n const pendingTieOffs = new Map<string, MidiEvent>();\n const legatoOutIds = collectPartLegatoOut(part);\n const pendingLegato = new Map<string, MidiEvent[]>();\n\n // Staff-local synchronous meters (`_x.viritura.staffMeters`), resolved once\n // per part against the (unexpanded) measure order so repeat/jump expansion\n // in `measureOrder` doesn't need to recompute inherited state.\n const staffMeterTable = resolvePartStaffMeterTable(part, globalMeasures);\n\n // Coupled dynamics: a single source of truth for velocity (sampled per note)\n // and CC11 (emitted up front so it spans note boundaries for held-note shaping).\n const dynamicProgram = compileDynamicProgram(\n part,\n partIdx,\n measureOrder,\n measureStartBeats,\n model,\n globalMeasures,\n gmProgram,\n impliedDynamics,\n staffMeterTable,\n );\n for (const lane of dynamicProgram.lanes.values()) {\n emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);\n }\n\n // Persistent pizz/arco + con sord. keyswitch state.\n const bowCapable = ARCO_CAPABLE_PROGRAMS.has(gmProgram);\n const muteFamily = muteFamilyForProgram(gmProgram);\n const techniqueEnabled = bowCapable || muteFamily !== null;\n let techniqueState: TechniqueState = { program: gmProgram, muted: false };\n\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const origMeasureIdx = measureOrder[expandedIdx]!;\n const partMeasure = part.measures[origMeasureIdx];\n if (!partMeasure) continue;\n\n const measureStartTime = measureStartTimes[expandedIdx]!;\n const measureStartBeat = measureStartBeats[expandedIdx]!;\n const activeTime = resolveActiveTime(globalMeasures, origMeasureIdx);\n const activeKeyFifths = resolveActiveKeyFifths(globalMeasures, origMeasureIdx);\n\n // Fermata/caesura holds for this measure shift later events within it.\n const measureHolds = holdSchedule[expandedIdx] ?? [];\n const measureSpq = model.spbAtBeat(measureStartBeat);\n // Each fermata hold is a merged span [startBeat, spanEndBeat) whose time\n // insertion lives in the tempo model at the driver-end beat (`atBeat`). A\n // carrier in the span rings through the gap automatically; the group bounds\n // let resolveFermataDuration force-extend a member that ends early.\n const fermataGroups = measureHolds\n .filter((h) => h.kind === \"fermata\")\n .map((h) => ({\n startBeat: h.startBeat ?? h.atBeat,\n spanEndBeat: h.spanEndBeat ?? h.atBeat,\n insertBeat: h.atBeat,\n }));\n\n const ctx: PartCtx = {\n tempoMap,\n model,\n measureStartBeat,\n partIndex: partIdx,\n channel,\n tieTargets,\n pendingTieOffs,\n legatoOutIds,\n pendingLegato,\n voiceKey: \"\",\n out: allEvents,\n gmProgram,\n timeSig: activeTime,\n staffMeterRatio: 1,\n keyFifths: activeKeyFifths,\n kitMidiMap,\n kitAltProgramMap,\n dynamicsEnvelope: dynamicProgram.lanes.values().next().value!.envelope,\n playbackLaneId: dynamicProgram.lanes.values().next().value!.id,\n dynamicProgram,\n sequenceStaff: 1,\n sequenceVoice: \"sequence:0\",\n measureSpq,\n fermataGroups,\n };\n\n if (techniqueEnabled) {\n techniqueState = applyMeasureTechniques(\n partMeasure.expressions,\n ctx,\n techniqueState,\n {\n bow: bowCapable,\n mute: muteFamily,\n },\n (staff) => staffMeterRatioAt(staffMeterTable, origMeasureIdx, staff),\n );\n }\n\n partMeasure.sequences.forEach((seq, seqIdx) => {\n // Per-voice identity for tie/legato pairing: the MNX voice id when present\n // (stable across measures so ties cross bar lines), else the sequence slot.\n ctx.voiceKey = seq.voice ?? `v${seqIdx}`;\n const lane = laneForSequence(dynamicProgram, partIdx, seq, seqIdx);\n ctx.sequenceStaff = seq.staff ?? 1;\n ctx.sequenceVoice = lane.voice;\n ctx.dynamicsEnvelope = lane.envelope;\n ctx.playbackLaneId = lane.id;\n const effectiveStaffMeter = staffMeterTable[origMeasureIdx]?.get(ctx.sequenceStaff);\n ctx.timeSig = effectiveStaffMeter ? effectiveStaffMeter.timeSignature : activeTime;\n ctx.staffMeterRatio = staffMeterRatioAt(staffMeterTable, origMeasureIdx, ctx.sequenceStaff);\n const firstNewEvent = allEvents.length;\n processSequence(ctx, seq, measureStartTime, measureHolds, measureSpq);\n stampPlaybackLane(allEvents, firstNewEvent, lane.id);\n stampPendingLane(ctx, lane.id);\n });\n }\n\n // Flush any remaining pending tie noteOffs\n for (const noteOff of pendingTieOffs.values()) {\n allEvents.push(noteOff);\n }\n\n // Flush deferred legato noteOffs left open at the end of the part, per voice (a\n // slur that runs to the final note with no following onset to overlap).\n flushAllLegato(pendingLegato, allEvents);\n}\n\nfunction stampPlaybackLane(events: MidiEvent[], startIndex: number, laneId: string): void {\n for (let i = startIndex; i < events.length; i++) events[i]!.playbackLaneId ??= laneId;\n}\n\nfunction stampPendingLane(ctx: PartCtx, laneId: string): void {\n for (const event of ctx.pendingTieOffs.values()) event.playbackLaneId ??= laneId;\n for (const events of ctx.pendingLegato.values()) {\n for (const event of events) event.playbackLaneId ??= laneId;\n }\n}\n\n/** Absolute time (s) of a measure-relative beat, via the continuous model. */\nfunction eventTime(ctx: PartCtx, beatOffset: number): number {\n return ctx.model.timeAtBeat(ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio);\n}\n\n/** Seconds spanned by `beats` starting at a measure-relative beat. */\nfunction eventSeconds(ctx: PartCtx, beatOffset: number, beats: number): number {\n return ctx.model.secondsForBeats(\n ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n beats * ctx.staffMeterRatio,\n );\n}\n\n/** Sample the part's coupled dynamic velocity (the noteOn level) at a measure beat. */\nfunction dynamicVelocityAt(ctx: PartCtx, measureStartTime: number, beatOffset: number): number {\n void measureStartTime;\n return noteVelocityAt(ctx.dynamicsEnvelope, eventTime(ctx, beatOffset));\n}\n\n/** Run one note event against its effective staff/voice dynamics lane and tag\n * every emitted or deferred MIDI event with that lane identity. */\nfunction processScopedNoteEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const previousEnvelope = ctx.dynamicsEnvelope;\n const previousLaneId = ctx.playbackLaneId;\n const lane = laneForScope(ctx.dynamicProgram, ctx.partIndex, event.staff ?? ctx.sequenceStaff, ctx.sequenceVoice);\n ctx.dynamicsEnvelope = lane.envelope;\n ctx.playbackLaneId = lane.id;\n const firstNewEvent = ctx.out.length;\n const beats = processNoteEvent(\n ctx,\n event,\n measureStartTime,\n beatOffset,\n dynamicVelocityAt(ctx, measureStartTime, beatOffset),\n tupletRatio,\n );\n stampPlaybackLane(ctx.out, firstNewEvent, lane.id);\n stampPendingLane(ctx, lane.id);\n ctx.dynamicsEnvelope = previousEnvelope;\n ctx.playbackLaneId = previousLaneId;\n return beats;\n}\n\n/**\n * Process a single sequence (voice), appending events to `ctx.out`.\n *\n * Fermata/caesura time insertions are baked into the continuous tempo model, so\n * every event's absolute time (`eventTime`) already reflects holds at any\n * nesting depth \u2014 no per-item offset bookkeeping is needed here.\n */\nfunction processSequence(\n ctx: PartCtx,\n seq: Sequence,\n measureStartTime: number,\n measureHolds: readonly MeasureHold[],\n spq: number,\n): void {\n void measureHolds;\n void spq;\n if (seq.fullMeasure) return;\n\n let beatCursor = 0;\n for (const item of seq.content) {\n beatCursor += processContentItem(ctx, item, measureStartTime, beatCursor, 1.0);\n }\n}\n\n/**\n * Process a single content item. Returns the number of beats consumed.\n *\n * The noteOn velocity (dynamic level) is sampled from the coupled dynamics\n * envelope at each event's onset, so it is position-aware within the measure.\n */\nfunction processContentItem(\n ctx: PartCtx,\n item: SequenceContent,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n switch (item.type) {\n case \"event\":\n return processScopedNoteEvent(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"tuplet\":\n return processTuplet(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"grace\":\n return processGrace(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n case \"space\":\n // Space: advance time without emitting events\n // duration is [numerator, denominator] as a fraction of a whole note\n return (item.duration[0] / item.duration[1]) * 4 * tupletRatio;\n\n case \"tremolo\":\n return processTremolo(ctx, item, measureStartTime, beatOffset, tupletRatio);\n\n default:\n return 0;\n }\n}\n\n/**\n * Resolve a note's sounding duration and articulation gap scale.\n *\n * The fermata's time insertion lives in the tempo model at the group's\n * driving-note end, so a held carrier's MODEL duration already spans the gap \u2014\n * it rings through automatically (the followers shifted with it at any nesting\n * depth, so there is no \"bleed\"). We only force-extend a group member that ends\n * BEFORE the insertion beat up to the group's resume time, so staggered-end\n * fermatas still release together. A bar may have several disjoint groups; the\n * carrier is matched by onset beat. A held note isn't detached \u2192 gap scale 1.\n */\nfunction resolveFermataDuration(\n ctx: PartCtx,\n event: NoteEvent,\n naturalDurationSec: number,\n beatOffset: number,\n): { durationSec: number; durationScale: number; held: boolean } {\n if (event.fermata && event.fermata.duration !== \"none\" && ctx.fermataGroups.length > 0) {\n // Hold groups are expressed on the shared global axis. Convert this\n // staff's written onset before matching it to its ensemble group.\n const globalBeatOffset = beatOffset * ctx.staffMeterRatio;\n const group = ctx.fermataGroups.find(\n (g) => globalBeatOffset >= g.startBeat - 1e-9 && globalBeatOffset < g.spanEndBeat - 1e-9,\n );\n if (group) {\n // Resume time = model time at the (post-insertion) driver-end beat. For\n // the driver and any member ending at/after it, naturalDurationSec already\n // reaches this (the gap is inside its span); a member ending earlier is\n // extended up to it so the group releases together.\n const startTime = eventTime(ctx, beatOffset);\n const resumeTime = ctx.model.timeAtBeat(ctx.measureStartBeat + group.insertBeat);\n return { durationSec: Math.max(naturalDurationSec, resumeTime - startTime), durationScale: 1, held: true };\n }\n }\n return { durationSec: naturalDurationSec, durationScale: articulationDurationScale(event.markings), held: false };\n}\n\n/** Whether this event is interior to a slur span (legato-connected to the next note). */\nfunction isInteriorSlurNote(ctx: PartCtx, event: NoteEvent): boolean {\n return event.id !== undefined && ctx.legatoOutIds.has(event.id);\n}\n\n/**\n * Dispatch an event to a special playback mode (percussion roll, pitched\n * tremolo, or trill). Returns the consumed beats when one handled the event, or\n * `null` to fall through to the plain note path.\n *\n * Order matters: the percussion-roll branch runs before the pitched-tremolo\n * branch because the latter only subdivides `event.notes` and would silently\n * drop a kit-note roll (kit hits live in `event.kitNotes`).\n */\nfunction tryProcessSpecialEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n beats: number,\n): number | null {\n // Percussion roll: a tremolo on a kit-note sustains a single roll note for\n // the full duration instead of repeated hits (the percussion analog of the\n // 3-slash string tremolo \u2192 Tremolo Strings swap).\n if (ctx.kitMidiMap && event.markings?.tremolo && (event.kitNotes?.length ?? 0) > 0) {\n return processKitRoll(ctx, event, measureStartTime, beatOffset, baseVelocity, beats);\n }\n\n // Single-note pitched tremolo: subdivide into rapid repeated notes.\n const tremoloMarks = event.markings?.tremolo?.marks;\n if (tremoloMarks && tremoloMarks >= 1 && tremoloMarks <= 3) {\n return processSingleNoteTremolo(ctx, event, measureStartTime, beatOffset, baseVelocity, beats, tremoloMarks);\n }\n\n // Trill: alternate the principal with its diatonic upper auxiliary. Falls\n // through (returns null) if the duration is too short to trill.\n if (event.markings?.trill && tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity, beats)) {\n return beats;\n }\n\n return null;\n}\n\n/**\n * Process a NoteEvent \u2014 emit noteOn/noteOff pairs for each note.\n * Handles ties by deferring noteOff and suppressing noteOn for tied-to notes.\n * Handles single-note tremolos by subdividing into rapid repeated notes.\n */\nfunction processNoteEvent(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n tupletRatio: number,\n): number {\n const { partIndex, channel, tieTargets, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx;\n const beats = durationBeats(event.duration) * tupletRatio;\n\n if (isRest(event)) {\n // A slur never bridges a rest: release this voice's deferred legato note at\n // its natural time rather than overlapping into the silence.\n flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out);\n return beats;\n }\n\n // Resolve any deferred legato noteOffs from the previous note against this\n // note's onset (the \"next\" note from their perspective). Done up front so it\n // also covers the tremolo/trill branches below, which return early.\n resolveVoiceLegato({\n map: ctx.pendingLegato,\n voiceKey: ctx.voiceKey,\n out: ctx.out,\n model: ctx.model,\n measureStartBeat: ctx.measureStartBeat,\n partIndex: ctx.partIndex,\n measureStartTime,\n beatOffset,\n beats,\n notes: event.notes ?? [],\n });\n\n // Special playback modes (percussion roll / pitched tremolo / trill) handle\n // the event themselves and short-circuit the plain note path below.\n const specialBeats = tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, beats);\n if (specialBeats !== null) return specialBeats;\n\n const startTime = eventTime(ctx, beatOffset);\n const timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const naturalDurationSec = eventSeconds(ctx, beatOffset, beats);\n // A fermata holds the note in place (see resolveFermataDuration); otherwise\n // normal articulation scaling applies.\n const {\n durationSec,\n durationScale,\n held: fermataHeld,\n } = resolveFermataDuration(ctx, event, naturalDurationSec, beatOffset);\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n\n // Interior slur note: defer its release so it overlaps the next note (legato).\n // A fermata-held note is exempt: it must ring through its hold rather than be\n // pinned to the next note's onset by the legato resolver (which would discard\n // the fermata extension and release the note at its natural end).\n const legatoOut = !fermataHeld && isInteriorSlurNote(ctx, event);\n\n for (const note of event.notes ?? []) {\n // Pitches in the Viritura model are stored as concert (sounding) pitch.\n // The part's transposition is rendering metadata only \u2014 no adjustment needed.\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n\n const noteId = note.id ?? \"\";\n const isTiedTo = tieTargets.has(noteId);\n const hasTieOut = note.ties?.some((t) => t.target) ?? false;\n\n if (isTiedTo) {\n extendOrFinalizeTie(ctx, midiNote, startTime, durationSec, durationScale, hasTieOut);\n } else {\n emitFreshNote(\n ctx,\n midiNote,\n startTime,\n timingOffset,\n durationSec,\n durationScale,\n velocity,\n hasTieOut,\n legatoOut,\n {\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n },\n );\n }\n }\n\n // Kit notes (MNX `kitNotes`): always on the GM drum channel.\n if (kitMidiMap) {\n const jitteredStart = Math.max(0, startTime + timingOffset);\n for (const kn of event.kitNotes ?? []) {\n const midiNote = kitNoteToMidi(kn, kitMidiMap);\n if (midiNote < 0 || midiNote > 127) continue;\n // A component may borrow a sound from another GS kit (e.g. Tam-tam from\n // the Ethnic kit); that routes the hit to a dedicated alt drum channel.\n const drumKitProgram = kitAltProgramMap?.get(kn.kitComponent);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n drumKitProgram,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + durationSec * durationScale,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n drumKitProgram,\n });\n }\n }\n\n return beats;\n}\n\n/**\n * Tie-target branch of processNoteEvent: this note continues from a tie, so\n * suppress its noteOn and either extend the pending noteOff (if the chain\n * keeps going) or finalize it with articulation scaling.\n */\nfunction extendOrFinalizeTie(\n ctx: PartCtx,\n midiNote: number,\n startTime: number,\n durationSec: number,\n durationScale: number,\n hasTieOut: boolean,\n): void {\n const { partIndex, channel, pendingTieOffs, out } = ctx;\n const tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`;\n const pending = pendingTieOffs.get(tieKey);\n if (pending) {\n if (hasTieOut) {\n // Chain continues: extend the pending noteOff further\n pending.time = startTime + durationSec;\n } else {\n // Chain ends: emit the noteOff now with articulation scaling\n pending.time = startTime + durationSec * durationScale;\n out.push(pending);\n pendingTieOffs.delete(tieKey);\n }\n }\n // Register outgoing tie if chain continues\n if (hasTieOut && !pendingTieOffs.has(tieKey)) {\n pendingTieOffs.set(tieKey, {\n type: \"noteOff\",\n time: startTime + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n}\n\n/**\n * Fresh-note branch of processNoteEvent: emit a noteOn with timing jitter\n * for humanization, then either defer the noteOff (if a tie chain starts\n * here) or emit it inline with articulation scaling.\n */\ninterface ScoreNoteMetadata {\n scoreBeat: number;\n scoreDurationBeats: number;\n scoreEventId?: string;\n}\n\nfunction emitFreshNote(\n ctx: PartCtx,\n midiNote: number,\n startTime: number,\n timingOffset: number,\n durationSec: number,\n durationScale: number,\n velocity: number,\n hasTieOut: boolean,\n legatoOut: boolean,\n metadata: ScoreNoteMetadata,\n): void {\n const { partIndex, channel, pendingTieOffs, out } = ctx;\n // Clamp to 0 so early notes don't get negative times.\n const jitteredStart = Math.max(0, startTime + timingOffset);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n ...metadata,\n });\n\n if (hasTieOut) {\n // Start of a tie chain \u2014 defer noteOff (no jitter on ties).\n const tieKey = `${midiNote}:${partIndex}:${ctx.voiceKey}`;\n pendingTieOffs.set(tieKey, {\n type: \"noteOff\",\n time: startTime + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n } else {\n // Standalone or interior-slur note. Build the noteOff at its natural\n // (articulated) release; an interior-slur note is deferred so resolveLegato\n // can later push the release past the next onset (legato overlap).\n const off: MidiEvent = {\n type: \"noteOff\",\n time: jitteredStart + durationSec * durationScale,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n };\n if (legatoOut) pushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, off);\n else out.push(off);\n }\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Single-note tremolo\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Beats per subdivision for each tremolo slash count. */\nconst TREMOLO_SUBDIV_BEATS: Record<number, number> = {\n 1: 0.5, // 8th notes\n 2: 0.25, // 16th notes\n 3: 0.125, // 32nd notes\n};\n\n/** GM solo string programs (40\u201343). */\nconst STRING_SOLO_RANGE_LO = 40;\nconst STRING_SOLO_RANGE_HI = 43;\n/** GM program 44 \u2014 Tremolo Strings. */\nconst GM_TREMOLO_STRINGS = 44;\n\n/**\n * Sustained \"roll\" sample for a base drum MIDI note. A tremolo (roll) on a\n * kit-note plays this single sustained note instead of repeated hits \u2014 the\n * percussion analog of the 3-slash string tremolo \u2192 Tremolo Strings swap.\n * Keyed by the kit-component's resolved GM percussion note. Drums without an\n * entry simply sustain their own sound (e.g. a suspended-cymbal roll).\n *\n * 38 (Acoustic Snare) \u2192 25 (Snare Roll): present in the GS Standard and\n * Orchestra kits this app loads from Shan-SGM-Pro-15.sf2.\n */\nconst KIT_ROLL_MIDI: Record<number, number> = {\n 38: 25, // Acoustic Snare \u2192 Snare Roll\n};\n\n/**\n * Process a percussion roll \u2014 a tremolo marking on a kit-note event. Emits one\n * sustained note per kit-note spanning the full written duration (no\n * subdivision), swapping to a dedicated roll sample when one exists\n * ({@link KIT_ROLL_MIDI}). This keeps a notated roll as a single long note that\n * triggers the roll/sustain sample rather than a machine-gun of repeated hits.\n */\nfunction processKitRoll(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n beats: number,\n): number {\n const { partIndex, channel, out, kitMidiMap, kitAltProgramMap, timeSig } = ctx;\n if (!kitMidiMap) return beats;\n\n const startTime = eventTime(ctx, beatOffset);\n const timingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const jitteredStart = Math.max(0, startTime + timingOffset);\n const durationSec = eventSeconds(ctx, beatOffset, beats);\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n\n for (const kn of event.kitNotes ?? []) {\n const baseMidi = kitNoteToMidi(kn, kitMidiMap);\n if (baseMidi < 0 || baseMidi > 127) continue;\n const midiNote = KIT_ROLL_MIDI[baseMidi] ?? baseMidi;\n const drumKitProgram = kitAltProgramMap?.get(kn.kitComponent);\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n drumKitProgram,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: beats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + durationSec,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n drumKitProgram,\n });\n }\n\n return beats;\n}\n\n/**\n * Process a single-note tremolo \u2014 subdivide one note into rapid repeated notes.\n *\n * - 1 slash: subdivide into 8th notes\n * - 2 slashes: subdivide into 16th notes\n * - 3 slashes on string instruments (GM 40\u201343): use GM 45 Tremolo Strings sound\n * - 3 slashes on other instruments: subdivide into 32nd notes\n */\nfunction processSingleNoteTremolo(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n totalBeats: number,\n marks: number,\n): number {\n const { partIndex, channel, out, gmProgram, timeSig } = ctx;\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n const notes = event.notes ?? [];\n\n // 3-slash tremolo on solo strings: use Tremolo Strings GM patch\n if (marks === 3 && gmProgram >= STRING_SOLO_RANGE_LO && gmProgram <= STRING_SOLO_RANGE_HI) {\n const startTime = eventTime(ctx, beatOffset);\n const tremTimingOffset = timingHumanize(measureStartTime, beatOffset, partIndex);\n const jitteredTremStart = Math.max(0, startTime + tremTimingOffset);\n const durationSec = eventSeconds(ctx, beatOffset, totalBeats);\n\n // Program change \u2192 Tremolo Strings\n out.push({\n type: \"programChange\",\n time: jitteredTremStart - 0.001, // slightly before the note\n midiNote: 0,\n velocity: 0,\n partIndex,\n channel,\n program: GM_TREMOLO_STRINGS,\n });\n\n for (const note of notes) {\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n out.push({\n type: \"noteOn\",\n time: jitteredTremStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: totalBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({ type: \"noteOff\", time: jitteredTremStart + durationSec, midiNote, velocity: 0, partIndex, channel });\n }\n\n // Program change \u2192 restore original sound\n out.push({\n type: \"programChange\",\n time: startTime + durationSec + 0.001,\n midiNote: 0,\n velocity: 0,\n partIndex,\n channel,\n program: gmProgram,\n });\n\n return totalBeats;\n }\n\n // Measured tremolo: subdivide into repeated notes\n const subdivBeats = TREMOLO_SUBDIV_BEATS[marks] ?? 0.125;\n const numSubdivisions = Math.max(1, Math.round(totalBeats / subdivBeats));\n const actualSubdivBeats = totalBeats / numSubdivisions;\n\n for (let s = 0; s < numSubdivisions; s++) {\n const subBeatOffset = beatOffset + s * actualSubdivBeats;\n const subStartTime = eventTime(ctx, subBeatOffset);\n const subTimingOffset = timingHumanize(measureStartTime, subBeatOffset, partIndex);\n const jitteredSubStart = Math.max(0, subStartTime + subTimingOffset);\n const subDurationSec = eventSeconds(ctx, subBeatOffset, actualSubdivBeats);\n\n for (const note of notes) {\n const midiNote = noteToMidi(note);\n if (midiNote < 0 || midiNote > 127) continue;\n out.push({\n type: \"noteOn\",\n time: jitteredSubStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: actualSubdivBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredSubStart + subDurationSec * 0.9,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n }\n\n return totalBeats;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Trill\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Ideal wall-clock period per trill note (~14 attacks/sec). */\nconst TRILL_TARGET_PERIOD_SEC = 0.07;\n/** A trill note shorter than this reads as a click, not a pitch \u2014 don't emit it. */\nconst TRILL_MIN_NOTE_SEC = 0.05;\n\n/** Diatonic step cycle (C..B) for resolving the upper auxiliary. */\nconst STEP_CYCLE: readonly Step[] = [\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"];\n\n/** Chromatic alteration a key signature imposes on a given step (+1 sharp, -1 flat, 0 natural). */\nfunction keyAlterForStep(step: Step, fifths: number): number {\n if (fifths > 0) return SHARP_ORDER.slice(0, fifths).includes(step) ? 1 : 0;\n if (fifths < 0) return FLAT_ORDER.slice(0, -fifths).includes(step) ? -1 : 0;\n return 0;\n}\n\n/**\n * MIDI number of the trill's upper auxiliary for a principal pitch.\n *\n * The auxiliary is the diatonic step above, altered by the key signature so\n * the trill stays in-scale \u2014 unless the trill carries an explicit accidental,\n * which signals a non-diatonic (chromatic) auxiliary.\n */\nfunction trillUpperMidi(pitch: Pitch, keyFifths: number, trillAccidental: number | undefined): number {\n const idx = STEP_CYCLE.indexOf(pitch.step);\n const upStep = STEP_CYCLE[(idx + 1) % 7]!;\n const upOctave = (idx === 6 ? pitch.octave + 1 : pitch.octave) as Octave;\n const alter = trillAccidental ?? keyAlterForStep(upStep, keyFifths);\n return pitchToMidi({ step: upStep, octave: upOctave, alter });\n}\n\n/**\n * Expand a trilled note into rapid alternation between the principal note and\n * its diatonic upper auxiliary.\n *\n * Rate is computed in wall-clock time (\u224870 ms per note) rather than a fixed\n * note value, so the trill speed stays musical across tempos. The alternation\n * count is then snapped so every note divides the written duration evenly \u2014\n * keeping all notes (including the last) the same length and ending the trill\n * cleanly on the note boundary.\n *\n * Starts on the principal (lower) note. If the note is too short to fit even a\n * single perceptible alternation, returns `false` so the caller plays it plain\n * (i.e. the trill is omitted rather than emitting an imperceptible blur).\n */\nfunction tryProcessTrill(\n ctx: PartCtx,\n event: NoteEvent,\n measureStartTime: number,\n beatOffset: number,\n baseVelocity: number,\n totalBeats: number,\n): boolean {\n const { partIndex, channel, out, timeSig, keyFifths } = ctx;\n const notes = event.notes ?? [];\n if (notes.length === 0) return false;\n\n const durationSec = eventSeconds(ctx, beatOffset, totalBeats);\n\n // Even division by the wall-clock target, then back off until each note\n // clears the perceptibility floor. Fewer than 2 notes can't read as a trill.\n let n = Math.round(durationSec / TRILL_TARGET_PERIOD_SEC);\n while (n >= 2 && durationSec / n < TRILL_MIN_NOTE_SEC) n--;\n if (n < 2) return false;\n\n const subBeats = totalBeats / n;\n const velocity = Math.min(\n 127,\n Math.max(\n 1,\n applyArticulationVelocity(baseVelocity, event.markings) +\n metricAccentOffset(beatOffset, timeSig) +\n velocityHumanize(measureStartTime, beatOffset),\n ),\n );\n const trillAccidental = event.markings?.trill?.accidental;\n\n // Principal + upper auxiliary MIDI for each note in the (possibly chord) event.\n const pairs = notes\n .map((note) => ({\n lower: pitchToMidi(note.pitch),\n upper: trillUpperMidi(note.pitch, keyFifths, trillAccidental),\n }))\n .filter((p) => p.lower >= 0 && p.lower <= 127 && p.upper >= 0 && p.upper <= 127);\n if (pairs.length === 0) return false;\n\n for (let s = 0; s < n; s++) {\n // Start on the principal (lower) note: even steps lower, odd steps upper.\n const useUpper = s % 2 === 1;\n const subBeatOffset = beatOffset + s * subBeats;\n const subStartTime = eventTime(ctx, subBeatOffset);\n const subTimingOffset = timingHumanize(measureStartTime, subBeatOffset, partIndex);\n const jitteredStart = Math.max(0, subStartTime + subTimingOffset);\n const subDurationSec = eventSeconds(ctx, subBeatOffset, subBeats);\n\n for (const pair of pairs) {\n const midiNote = useUpper ? pair.upper : pair.lower;\n out.push({\n type: \"noteOn\",\n time: jitteredStart,\n midiNote,\n velocity,\n partIndex,\n channel,\n scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio,\n scoreDurationBeats: subBeats * ctx.staffMeterRatio,\n scoreEventId: event.id,\n });\n out.push({\n type: \"noteOff\",\n time: jitteredStart + subDurationSec * 0.9,\n midiNote,\n velocity: 0,\n partIndex,\n channel,\n });\n }\n }\n\n return true;\n}\n\n/**\n * Process a grace note group.\n *\n * Grace note timing follows industry-standard engravers conventions:\n * - makeTime (default) / stealPrevious: grace notes are played just before\n * the beat, stealing time from the preceding duration. The main timeline\n * cursor is NOT advanced.\n * - stealFollowing: grace notes are played at the current beat, pushing the\n * following note later. The cursor IS advanced by the total grace duration.\n *\n * Each grace note receives a fixed short duration (GRACE_NOTE_BEATS \u2248 32nd\n * note) regardless of its written value.\n */\nfunction processGrace(\n ctx: PartCtx,\n grace: Grace,\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const events = grace.content;\n if (events.length === 0) return 0;\n\n const graceBeats = GRACE_NOTE_BEATS * tupletRatio;\n const totalGraceBeats = events.length * graceBeats;\n\n if (grace.graceType === \"stealFollowing\") {\n // Play at current position \u2014 main note is pushed later\n let cursor = beatOffset;\n for (const evt of events) {\n const ratio = graceBeats / durationBeats(evt.duration);\n processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio);\n cursor += graceBeats;\n }\n return totalGraceBeats;\n }\n\n // makeTime (default) / stealPrevious: play before the beat\n let cursor = beatOffset - totalGraceBeats;\n for (const evt of events) {\n const ratio = graceBeats / durationBeats(evt.duration);\n processScopedNoteEvent(ctx, evt, measureStartTime, cursor, ratio);\n cursor += graceBeats;\n }\n return 0;\n}\n\n/**\n * Process a tuplet \u2014 apply the tuplet ratio and recurse into content.\n */\nfunction processTuplet(\n ctx: PartCtx,\n tuplet: {\n inner: { duration: Duration; multiple: number };\n outer: { duration: Duration; multiple: number };\n content: SequenceContent[];\n },\n measureStartTime: number,\n beatOffset: number,\n parentTupletRatio: number,\n): number {\n // Tuplet ratio: outer beats / inner beats\n // e.g., triplet: 3 eighths in the space of 2 \u2192 ratio = 2/3\n const outerBeats = durationBeats(tuplet.outer.duration) * tuplet.outer.multiple;\n const innerBeats = durationBeats(tuplet.inner.duration) * tuplet.inner.multiple;\n const ratio = (outerBeats / innerBeats) * parentTupletRatio;\n\n let cursor = beatOffset;\n for (const item of tuplet.content) {\n cursor += processContentItem(ctx, item, measureStartTime, cursor, ratio);\n }\n\n return outerBeats * parentTupletRatio;\n}\n\n/**\n * Process a multi-note tremolo \u2014 alternate between the two notes.\n */\nfunction processTremolo(\n ctx: PartCtx,\n tremolo: {\n content: NoteEvent[];\n marks: number;\n outer: { duration: Duration; multiple: number };\n },\n measureStartTime: number,\n beatOffset: number,\n tupletRatio: number,\n): number {\n const outerBeats = durationBeats(tremolo.outer.duration) * tremolo.outer.multiple * tupletRatio;\n\n if (tremolo.content.length < 2) return outerBeats;\n\n const subdivisions = Math.pow(2, tremolo.marks);\n const subdivBeats = outerBeats / subdivisions;\n\n for (let s = 0; s < subdivisions; s++) {\n const noteEvent = tremolo.content[s % 2]!;\n const subBeatOffset = beatOffset + s * subdivBeats;\n const subRatio = subdivBeats / durationBeats(noteEvent.duration);\n\n processScopedNoteEvent(ctx, noteEvent, measureStartTime, subBeatOffset, subRatio);\n }\n\n return outerBeats;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Active time signature resolution\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Resolve the active key signature (circle-of-fifths) at a given measure index. */\nfunction resolveActiveKeyFifths(globalMeasures: readonly GlobalMeasure[], measureIdx: number): number {\n for (let m = measureIdx; m >= 0; m--) {\n const key = globalMeasures[m]!.key;\n if (key) return key.fifths;\n }\n return 0;\n}\n", "/**\n * Repeat & jump expansion \u2014 converts a `GlobalMeasure[]` into a linear\n * sequence of measure indices that the MIDI generator can walk straight\n * through.\n *\n * Extracted from `timeline.ts` to keep that file under the max-lines budget.\n */\n\nimport type { GlobalMeasure, Score } from \"@viritura/core\";\n\ninterface MeasureOrderHints {\n toCodaMeasureIndex?: number;\n}\n\n/**\n * Expand repeats and jumps into a linear sequence of measure indices.\n *\n * Handles:\n * - Simple repeats (repeatStart / repeatEnd)\n * - Volta brackets (endings with numbers)\n * - Jump markers (D.S. al Fine, D.S. al Coda, D.C. al Coda)\n */\nexport function expandMeasureOrder(globalMeasures: readonly GlobalMeasure[], hints?: MeasureOrderHints): number[] {\n if (globalMeasures.length === 0) return [];\n\n // Phase 1: expand repeats with volta endings\n const order = expandRepeats(globalMeasures);\n\n // Phase 2: handle jumps (D.S./D.C.)\n return expandJumps(globalMeasures, order, hints?.toCodaMeasureIndex);\n}\n\nexport function detectToCodaMeasureIndex(score: Pick<Score, \"parts\">): number {\n for (const part of score.parts) {\n for (let i = 0; i < part.measures.length; i++) {\n const expressions = part.measures[i]?.expressions;\n if (!expressions || expressions.length === 0) continue;\n if (expressions.some((expr) => /\\bto\\s*coda\\b/i.test(expr.text))) {\n return i;\n }\n }\n }\n return -1;\n}\n\n/** A volta ending spanning measure indices [startIdx, endIdx], taken on the listed pass numbers. */\ninterface EndingInfo {\n startIdx: number;\n endIdx: number;\n numbers: number[];\n}\n\n/**\n * Append one repeat-pass to `order`, skipping measures that belong to a\n * volta ending whose `numbers` list doesn't include the current pass.\n * Extracted so the volta loop in `expandRepeats` doesn't exceed max-depth.\n */\nfunction appendVoltaPass(\n order: number[],\n endings: readonly EndingInfo[],\n pass: number,\n startIdx: number,\n endIdx: number,\n): void {\n for (let j = startIdx; j <= endIdx; j++) {\n const inEnding = endings.find((e) => j >= e.startIdx && j <= e.endIdx);\n if (inEnding && !inEnding.numbers.includes(pass)) continue;\n order.push(j);\n }\n}\n\n/**\n * Expand repeat markers and volta endings into a linear measure order.\n */\nfunction expandRepeats(globalMeasures: readonly GlobalMeasure[]): number[] {\n const order: number[] = [];\n const len = globalMeasures.length;\n let i = 0;\n\n while (i < len) {\n const gm = globalMeasures[i]!;\n\n if (gm.repeatEnd) {\n const repeatTimes = gm.repeatEnd.times ?? 2;\n\n // Find the matching repeatStart (scan backwards from current)\n let repeatStartIdx = 0;\n for (let j = i; j >= 0; j--) {\n if (globalMeasures[j]!.repeatStart) {\n repeatStartIdx = j;\n break;\n }\n }\n\n // Remove measures already added from the repeat range\n // (they were added as regular measures before we knew about the repeat)\n while (order.length > 0 && order[order.length - 1]! >= repeatStartIdx && order[order.length - 1]! <= i) {\n order.pop();\n }\n\n // Collect volta endings in this repeat section\n const endings: EndingInfo[] = [];\n for (let j = repeatStartIdx; j <= i; j++) {\n const ending = globalMeasures[j]!.ending;\n if (ending) {\n endings.push({\n startIdx: j,\n endIdx: Math.min(j + ending.duration - 1, i),\n numbers: ending.numbers,\n });\n }\n }\n\n if (endings.length > 0) {\n for (let pass = 1; pass <= repeatTimes; pass++) {\n appendVoltaPass(order, endings, pass, repeatStartIdx, i);\n }\n } else {\n for (let pass = 0; pass < repeatTimes; pass++) {\n for (let j = repeatStartIdx; j <= i; j++) {\n order.push(j);\n }\n }\n }\n\n i++;\n } else {\n order.push(i);\n i++;\n }\n }\n\n return order;\n}\n\nfunction appendRange(order: number[], start: number, endInclusive: number): void {\n for (let measure = start; measure <= endInclusive; measure++) order.push(measure);\n}\n\nfunction appendAlCodaReplay(\n result: number[],\n replayStart: number,\n codaIdx: number,\n measureCount: number,\n toCodaMeasureIdx: number | undefined,\n): void {\n const replayEnd =\n toCodaMeasureIdx !== undefined && toCodaMeasureIdx >= replayStart && toCodaMeasureIdx < codaIdx\n ? toCodaMeasureIdx\n : codaIdx - 1;\n appendRange(result, replayStart, replayEnd);\n appendRange(result, codaIdx, measureCount - 1);\n}\n\n/**\n * Process jump markers in the measure order.\n */\nfunction expandJumps(\n globalMeasures: readonly GlobalMeasure[],\n initialOrder: readonly number[],\n toCodaMeasureIdx: number | undefined,\n): number[] {\n let segnoIdx = -1;\n let fineIdx = -1;\n let codaIdx = -1;\n\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n if (gm.segno) segnoIdx = m;\n if (gm.fine) fineIdx = m;\n if (gm.coda) codaIdx = m;\n }\n\n // Find the first jump in the expanded order\n let jumpOrderPos = -1;\n let jumpMeasureIdx = -1;\n for (let p = 0; p < initialOrder.length; p++) {\n const mi = initialOrder[p]!;\n if (globalMeasures[mi]!.jump) {\n jumpOrderPos = p;\n jumpMeasureIdx = mi;\n break;\n }\n }\n\n if (jumpOrderPos === -1) return [...initialOrder];\n\n const jump = globalMeasures[jumpMeasureIdx]!.jump!;\n const result = initialOrder.slice(0, jumpOrderPos + 1);\n\n switch (jump.type) {\n case \"dsalfine\":\n if (segnoIdx >= 0 && fineIdx >= 0) appendRange(result, segnoIdx, fineIdx);\n break;\n case \"dsalcoda\":\n if (segnoIdx >= 0 && codaIdx >= 0)\n appendAlCodaReplay(result, segnoIdx, codaIdx, globalMeasures.length, toCodaMeasureIdx);\n break;\n case \"dcalcoda\":\n if (codaIdx >= 0) appendAlCodaReplay(result, 0, codaIdx, globalMeasures.length, toCodaMeasureIdx);\n break;\n case \"segno\":\n if (segnoIdx >= 0) appendRange(result, segnoIdx, globalMeasures.length - 1);\n break;\n }\n\n return result;\n}\n", "/**\n * Measure repeats (simile marks) for playback.\n *\n * MNX puts a `measureRepeat` on the FIRST bar it covers and says \"repeat all\n * music in the previous N measures\". The covered bars carry no encoding of\n * their own and normally hold empty sequences, so playback has to substitute\n * the source material back in before anything downstream (notes, ties,\n * dynamics, holds) can see it.\n *\n * Substitution is deep-cloned with rewritten ids so a repeated bar does not\n * collide with its source in id-keyed lookups such as tie resolution.\n */\n\nimport type { Part, PartMeasure, Score } from \"@viritura/core\";\n\n/**\n * Map each part-measure index to the index whose music actually sounds there.\n *\n * A repeat at index `i` spanning `n` bars sources indices `i - n \u2026 i - 1`.\n * Chains resolve transitively, so a simile pointing at a bar that is itself a\n * simile reaches through to real music.\n */\nfunction buildSourceIndices(measures: readonly PartMeasure[]): number[] {\n const source = measures.map((_, index) => index);\n for (let index = 0; index < measures.length; index++) {\n const repeat = measures[index]?.measureRepeat;\n if (!repeat) continue;\n const span = Math.max(1, Math.trunc(repeat.number));\n const firstSource = index - span;\n if (firstSource < 0) continue;\n for (let offset = 0; offset < span && index + offset < measures.length; offset++) {\n // Resolve through the source's own mapping so chained similes collapse\n // to the nearest bar holding real music.\n source[index + offset] = source[firstSource + offset] ?? firstSource + offset;\n }\n }\n return source;\n}\n\n/** Rewrite every defined id in one clone and add it to the span-wide map. */\nfunction rewriteIds(node: unknown, suffix: string, remap: Map<string, string>): void {\n if (Array.isArray(node)) {\n for (const item of node) rewriteIds(item, suffix, remap);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const record = node as Record<string, unknown>;\n if (typeof record[\"id\"] === \"string\") {\n const original = record[\"id\"];\n const next = `${original}${suffix}`;\n remap.set(original, next);\n record[\"id\"] = next;\n }\n for (const value of Object.values(record)) rewriteIds(value, suffix, remap);\n}\n\n/** Relink every string reference that names an id cloned in this repeat span. */\nfunction relinkIds(node: unknown, remap: ReadonlyMap<string, string>): void {\n if (Array.isArray(node)) {\n for (const item of node) relinkIds(item, remap);\n return;\n }\n if (!node || typeof node !== \"object\") return;\n const record = node as Record<string, unknown>;\n for (const [key, value] of Object.entries(record)) {\n if (key === \"id\") continue;\n if (typeof value === \"string\") {\n const mapped = remap.get(value);\n if (mapped !== undefined) record[key] = mapped;\n } else {\n relinkIds(value, remap);\n }\n }\n}\n\n/**\n * Expand a part's simile marks into playable measures.\n *\n * Returns the part unchanged when it has no measure repeats, so the common\n * case costs one scan and no allocation.\n */\nexport function expandMeasureRepeats(part: Part): Part {\n if (!part.measures.some((measure) => measure.measureRepeat)) return part;\n\n const source = buildSourceIndices(part.measures);\n const measures = [...part.measures];\n for (let start = 0; start < part.measures.length; start++) {\n const repeat = part.measures[start]?.measureRepeat;\n if (!repeat) continue;\n const span = Math.max(1, Math.trunc(repeat.number));\n if (start < span) continue;\n\n const clones: Array<{ destination: number; measure: PartMeasure }> = [];\n for (let offset = 0; offset < span && start + offset < part.measures.length; offset++) {\n const destination = start + offset;\n const originIndex = source[destination];\n const origin = originIndex === undefined ? undefined : part.measures[originIndex];\n if (origin) clones.push({ destination, measure: structuredClone(origin) as PartMeasure });\n }\n\n // Build one remap for the complete repeated span before relinking. This is\n // what keeps cross-barline ties/slurs/glissandos inside the new iteration.\n const remap = new Map<string, string>();\n for (const clone of clones) rewriteIds(clone.measure, `~r${clone.destination}`, remap);\n for (const clone of clones) {\n relinkIds(clone.measure, remap);\n delete clone.measure.measureRepeat;\n measures[clone.destination] = clone.measure;\n }\n start += span - 1;\n }\n\n return { ...part, measures };\n}\n\n/**\n * Expand every part's simile marks so downstream passes see real music where a\n * measure repeat stands. Returns the score unchanged when no part has one.\n */\nexport function expandScoreMeasureRepeats(score: Score): Score {\n const parts = score.parts.map(expandMeasureRepeats);\n if (parts.every((part, index) => part === score.parts[index])) return score;\n return { ...score, parts };\n}\n", "import { CHORDS_PART_ID, voiceChordSymbol, type Score } from \"@viritura/core\";\nimport type { TempoBuild } from \"../tempoMap\";\nimport type { MidiEvent } from \"../types\";\n\n/** Runtime routing metadata only; never insert this lane into score.parts. */\nexport interface ChordPlaybackPart {\n id: typeof CHORDS_PART_ID;\n partIndex: number;\n name: \"Chords\";\n}\n\n/** Unsupported symbols and NC still require a visible, independently routed lane. */\nexport function getChordPlaybackPart(score: Score): ChordPlaybackPart | undefined {\n return score.global.measures.some((measure) => (measure.chordSymbols?.length ?? 0) > 0)\n ? { id: CHORDS_PART_ID, partIndex: score.parts.length, name: \"Chords\" }\n : undefined;\n}\n\ninterface ChordChange {\n beat: number;\n pitches: readonly number[];\n}\n\ninterface ChordMeasure {\n carry: readonly number[];\n changes: readonly ChordChange[];\n}\n\nexport interface ChordPlaybackProgram {\n part: ChordPlaybackPart;\n measures: readonly ChordMeasure[];\n}\n\nexport interface ChordPlaybackNote {\n id: string;\n pitch: number;\n startTime: number;\n duration: number;\n}\n\n/** Resolve harmony in written order, before repeats can change the carried state. */\nexport function compileChordPlayback(score: Score, includeGlobalChords = true): ChordPlaybackProgram | undefined {\n if (!includeGlobalChords) return undefined;\n const part = getChordPlaybackPart(score);\n if (!part) return undefined;\n let carry: readonly number[] = [];\n const measures = score.global.measures.map((measure): ChordMeasure => {\n const changes = (measure.chordSymbols ?? [])\n .map((chord): ChordChange => {\n const voicing = voiceChordSymbol(chord);\n return {\n beat: (chord.position.fraction[0] / chord.position.fraction[1]) * 4,\n pitches: [...voicing.leftHand, ...voicing.rightHand],\n };\n })\n .sort((a, b) => a.beat - b.beat);\n const result = { carry, changes };\n if (changes.length > 0) carry = changes[changes.length - 1]!.pitches;\n return result;\n });\n return { part, measures };\n}\n\n/** Continuous measures sustain; jumps release and restore the destination's written harmony. */\nexport function realizeChordPlayback(\n program: ChordPlaybackProgram,\n measureOrder: readonly number[],\n timing: TempoBuild,\n): ChordPlaybackNote[] {\n const notes: ChordPlaybackNote[] = [];\n let pitches: readonly number[] = [];\n let startBeat = 0;\n let occurrence = 0;\n const finish = (endBeat: number): void => {\n if (endBeat <= startBeat) return;\n const startTime = timing.model.timeAtBeat(startBeat);\n const duration = timing.model.timeAtBeat(endBeat) - startTime;\n for (const pitch of pitches) {\n notes.push({ id: `${CHORDS_PART_ID}:${occurrence}:${pitch}`, pitch, startTime, duration });\n }\n occurrence++;\n };\n for (let expandedIndex = 0; expandedIndex < measureOrder.length; expandedIndex++) {\n const originalIndex = measureOrder[expandedIndex]!;\n const measure = program.measures[originalIndex]!;\n const measureStart = timing.measureStartBeats[expandedIndex]!;\n if (expandedIndex === 0 || originalIndex !== measureOrder[expandedIndex - 1]! + 1) {\n finish(measureStart);\n pitches = measure.carry;\n startBeat = measureStart;\n }\n for (const change of measure.changes) {\n const beat = measureStart + change.beat;\n finish(beat);\n pitches = change.pitches;\n startBeat = beat;\n }\n }\n finish(timing.totalBeats);\n return notes;\n}\n\nexport function chordMidiEvents(\n program: ChordPlaybackProgram,\n measureOrder: readonly number[],\n timing: TempoBuild,\n): MidiEvent[] {\n return realizeChordPlayback(program, measureOrder, timing).flatMap((note): MidiEvent[] => {\n const routing = {\n partIndex: program.part.partIndex,\n playbackLaneId: program.part.id,\n channel: 0,\n midiNote: note.pitch,\n };\n return [\n { ...routing, type: \"noteOn\", time: note.startTime, velocity: 80 },\n { ...routing, type: \"noteOff\", time: note.startTime + note.duration, velocity: 0 },\n ];\n });\n}\n", "import type { DynamicGroup, DynamicValue, LayoutContent, PartMeasure, Score, SequenceContent } from \"@viritura/core\";\n\ninterface ExplicitState {\n value: DynamicValue;\n position: number;\n partIndex: number;\n measureIndex: number;\n}\n\nexport interface ImpliedSectionDynamicAnchor {\n partIndex: number;\n measureIndex: number;\n value: DynamicValue;\n sourcePartIndex: number;\n sourceMeasureIndex: number;\n position: [number, number];\n}\n\nfunction collectGroups(content: readonly LayoutContent[], score: Score, groups: number[][]): void {\n for (const node of content) {\n if (node.type === \"group\") {\n collectGroups(node.content, score, groups);\n continue;\n }\n if (node.sources.length < 2) continue;\n const indices = node.sources\n .map((source) => score.parts.findIndex((part) => part.id === source.part))\n .filter((index) => index >= 0);\n if (indices.length > 1) groups.push([...new Set(indices)]);\n }\n}\n\n/** Stable source groups are derived from every multi-source layout staff, not the active view. */\nfunction sectionGroups(score: Score): number[][] {\n const groups: number[][] = [];\n for (const layout of score.layouts ?? []) collectGroups(layout.content, score, groups);\n return [\n ...new Map(\n groups.map((group) => {\n const sorted = [...group].sort((a, b) => a - b);\n return [sorted.join(\",\"), sorted] as const;\n }),\n ).values(),\n ];\n}\n\nconst WHOLE_DURATIONS: Record<string, number> = {\n maxima: 8,\n longa: 4,\n breve: 2,\n whole: 1,\n half: 1 / 2,\n quarter: 1 / 4,\n eighth: 1 / 8,\n \"16th\": 1 / 16,\n \"32nd\": 1 / 32,\n \"64th\": 1 / 64,\n \"128th\": 1 / 128,\n \"256th\": 1 / 256,\n \"512th\": 1 / 512,\n \"1024th\": 1 / 1024,\n};\n\nfunction durationValue(duration: { base: string; dots?: number }): number {\n const base = WHOLE_DURATIONS[duration.base] ?? 0;\n let value = base;\n for (let dot = 0; dot < (duration.dots ?? 0); dot++) value += base / 2 ** (dot + 1);\n return value;\n}\n\ninterface ContentInfo {\n signature: unknown[];\n active: boolean;\n onset?: number;\n duration: number;\n}\n\nfunction contentInfo(content: readonly SequenceContent[], scale = 1): ContentInfo {\n let cursor = 0;\n let active = false;\n let onset: number | undefined;\n const signature = content.map((item) => {\n if (item.type === \"event\") {\n const sounding = !!item.notes?.length || !!item.kitNotes?.length;\n if (sounding) {\n active = true;\n onset ??= cursor;\n }\n const duration = durationValue(item.duration) * scale;\n cursor += duration;\n return {\n duration: item.duration,\n sounding,\n };\n }\n if (item.type === \"tuplet\") {\n const inner = durationValue(item.inner.duration) * item.inner.multiple;\n const outer = durationValue(item.outer.duration) * item.outer.multiple;\n const nested = contentInfo(item.content, scale * (inner > 0 ? outer / inner : 1));\n if (nested.active) {\n active = true;\n onset ??= cursor + nested.onset!;\n }\n cursor += nested.duration;\n return { type: item.type, inner: item.inner, outer: item.outer, content: nested.signature };\n }\n if (item.type === \"tremolo\") {\n const duration = durationValue(item.outer.duration) * item.outer.multiple * scale;\n if (item.content.some((event) => !!event.notes?.length || !!event.kitNotes?.length)) {\n active = true;\n onset ??= cursor;\n }\n cursor += duration;\n return { type: item.type, outer: item.outer, content: item.content.map((event) => event.duration) };\n }\n if (item.type === \"grace\") return { type: item.type, content: item.content.map((event) => event.duration) };\n if (item.type === \"space\") {\n const duration = (item.duration[1] === 0 ? 0 : item.duration[0] / item.duration[1]) * scale;\n cursor += duration;\n return { type: item.type, duration: item.duration };\n }\n return {};\n });\n return { signature, active, onset, duration: cursor };\n}\n\nfunction measureActivity(measure: PartMeasure | undefined): { active: boolean; rhythm: string; onset: number } {\n if (!measure) return { active: false, rhythm: \"\", onset: 0 };\n let active = false;\n let onset: number | undefined;\n const signature = measure.sequences.map((sequence) => {\n const content = contentInfo(sequence.content);\n if (content.active) {\n active = true;\n onset = Math.min(onset ?? Number.POSITIVE_INFINITY, content.onset ?? 0);\n }\n return { staff: sequence.staff, voice: sequence.voice, content: content.signature };\n });\n return { active, rhythm: JSON.stringify(signature), onset: onset ?? 0 };\n}\n\nfunction persistentValue(group: DynamicGroup): DynamicValue | undefined {\n if (group.type === \"immediate\") return group.value;\n if (group.type === \"accent\") return group.residualValue;\n if (group.type === \"gradual\") return group.value;\n return undefined;\n}\n\nfunction positionValue(group: DynamicGroup): number {\n const [numerator, denominator] = group.position.fraction;\n return denominator === 0 ? 0 : numerator / denominator;\n}\n\nfunction positionFraction(value: number): [number, number] {\n const denominator = 65536;\n const numerator = Math.round(value * denominator);\n const gcd = (left: number, right: number): number => {\n while (right !== 0) [left, right] = [right, left % right];\n return Math.abs(left) || 1;\n };\n const divisor = gcd(numerator, denominator);\n return [numerator / divisor, denominator / divisor];\n}\n\nfunction explicitStates(measure: PartMeasure | undefined, partIndex: number, measureIndex: number): ExplicitState[] {\n return (measure?.dynamics ?? [])\n .flatMap((group) => {\n const value = persistentValue(group);\n return value ? [{ value, position: positionValue(group), partIndex, measureIndex }] : [];\n })\n .sort((left, right) => left.position - right.position);\n}\n\nfunction agreedExplicit(states: readonly ExplicitState[]): ExplicitState | null | undefined {\n if (states.length === 0) return undefined;\n const values = new Set(states.map((state) => state.value));\n return values.size === 1 ? states[0]! : null;\n}\n\nfunction updateShared(\n states: ReadonlyMap<number, readonly ExplicitState[]>,\n activeParts: readonly number[],\n positions: readonly number[],\n ownState: Map<number, DynamicValue>,\n initial: ExplicitState | undefined,\n): ExplicitState | undefined {\n let shared = initial;\n for (const position of positions) {\n const activeAtPosition = activeParts.flatMap((partIndex) =>\n states.get(partIndex)!.filter((state) => state.position === position),\n );\n const agreement = agreedExplicit(activeAtPosition);\n if (agreement === null) shared = undefined;\n else if (agreement) shared = agreement;\n for (const partIndex of states.keys()) {\n // A dynamic written while this player rests still establishes that\n // player's own state; retain it so a later reentry never overwrites an\n // explicit instruction with a section inference.\n const own = states.get(partIndex)!.find((state) => state.position === position);\n if (own) ownState.set(partIndex, own.value);\n }\n }\n return shared;\n}\n\nfunction analyzeGroup(score: Score, group: readonly number[]): ImpliedSectionDynamicAnchor[] {\n const inferred: ImpliedSectionDynamicAnchor[] = [];\n const ownState = new Map<number, DynamicValue>();\n let shared: ExplicitState | undefined;\n\n for (let measureIndex = 0; measureIndex < score.global.measures.length; measureIndex++) {\n const activity = new Map(\n group.map((partIndex) => [partIndex, measureActivity(score.parts[partIndex]?.measures[measureIndex])]),\n );\n const activeParts = group.filter((partIndex) => activity.get(partIndex)?.active);\n const states = new Map(\n group.map((partIndex) => [\n partIndex,\n explicitStates(score.parts[partIndex]?.measures[measureIndex], partIndex, measureIndex),\n ]),\n );\n const merged =\n activeParts.length > 1 && new Set(activeParts.map((partIndex) => activity.get(partIndex)!.rhythm)).size === 1;\n const onset = merged ? Math.min(...activeParts.map((partIndex) => activity.get(partIndex)!.onset)) : 0;\n const positions = [\n ...new Set(group.flatMap((partIndex) => states.get(partIndex)!.map((state) => state.position))),\n ].sort((a, b) => a - b);\n shared = updateShared(\n states,\n activeParts,\n positions.filter((position) => !merged || position <= onset),\n ownState,\n shared,\n );\n if (merged && shared) {\n for (const partIndex of activeParts) {\n const hasExplicitAtOnset = states.get(partIndex)!.some((state) => state.position === onset);\n if (hasExplicitAtOnset || ownState.get(partIndex) === shared.value) continue;\n inferred.push({\n partIndex,\n measureIndex,\n value: shared.value,\n sourcePartIndex: shared.partIndex,\n sourceMeasureIndex: shared.measureIndex,\n position: positionFraction(onset),\n });\n ownState.set(partIndex, shared.value);\n }\n }\n\n shared = updateShared(\n states,\n activeParts,\n positions.filter((position) => merged && position > onset),\n ownState,\n shared,\n );\n }\n return inferred;\n}\n\n/** Compute playback-only implied anchors without creating notation objects. */\nexport function analyzeImpliedSectionDynamics(\n score: Score,\n): ReadonlyMap<number, readonly ImpliedSectionDynamicAnchor[]> {\n const inferred = sectionGroups(score).flatMap((group) => analyzeGroup(score, group));\n const byPart = new Map<number, ImpliedSectionDynamicAnchor[]>();\n for (const anchor of inferred) {\n const anchors = byPart.get(anchor.partIndex) ?? [];\n anchors.push(anchor);\n byPart.set(anchor.partIndex, anchors);\n }\n return byPart;\n}\n", "/**\n * Hold schedule \u2014 computes the extra time that fermatas and caesuras insert\n * into the timeline.\n *\n * A fermata holds the note it sits on (and pauses the ensemble); a caesura is\n * a grand-pause / breath that inserts silence. Both INSERT time at a beat\n * position, which shifts every later event in every part by the same amount.\n *\n * To keep the ensemble aligned, holds are computed once across ALL parts:\n * - Fermatas are grouped by OVERLAPPING time spans. Each fermata'd note spans\n * [onset, end); spans whose intervals overlap merge transitively into one\n * group (a held whole note bridges shorter fermatas under it). Non-overlapping\n * fermatas in the same bar (e.g. a hold on beat 1 and a separate hold on beat\n * 4) form SEPARATE groups \u2192 SEPARATE holds, so each ensemble pause is\n * preserved instead of being flattened to a single bar-end hold.\n * - Within each group the SHORTEST fermata'd note is the DRIVER: it defines\n * both the hold AMOUNT ((mult-1) \u00D7 its duration) AND the insertion POINT\n * (its END beat). The fermata is held IN PLACE at that note \u2014 the extra time\n * is inserted right where the short note ends, NOT dumped at the bar's end.\n * Longer fermata notes that overlap merely coexist as a hint: they sustain\n * through the inserted hold (each extends by the same `extraBeats` from its\n * own natural end) rather than each doubling their own long duration.\n * - Caesuras are max-merged per beat (a caesura at the same beat in two parts\n * is one pause).\n *\n * Holds are expressed in quarter-note BEATS (not seconds) so the tempo map can\n * convert them at the tempo in effect where they occur.\n */\n\nimport type { GlobalMeasure, Score, SequenceContent, Fermata, Duration, TimeSignature } from \"@viritura/core\";\nimport { DURATION_BEATS } from \"@viritura/core\";\nimport { resolvePartStaffMeterTable, staffMeterRatioAt } from \"./staffMeterTiming\";\n\n/** What inserted a hold \u2014 determines whether carriers sustain (fermata) or the\n * gap is pure silence (caesura). */\ntype HoldKind = \"fermata\" | \"caesura\";\n\n/** Extra time inserted at one beat position within a measure. */\nexport interface MeasureHold {\n /** Beat within the measure where the extra time is inserted. For a fermata\n * this is the DRIVING (shortest) note's END beat, so the hold lands in place\n * at that note rather than at the bar's end. For a caesura it is the\n * carrier's end. */\n atBeat: number;\n /** Extra duration to insert, in quarter-note beats. */\n extraBeats: number;\n /** Whether this hold is a fermata (held notes sustain through it) or a\n * caesura (a pure gap; carriers are not extended). */\n kind: HoldKind;\n /** Fermata only: the group's earliest fermata onset beat. A fermata'd carrier\n * whose onset falls in [startBeat, spanEndBeat) belongs to this group and\n * extends by `extraBeats`. */\n startBeat?: number;\n /** Fermata only: the group's latest fermata end beat (the span upper bound\n * for carrier matching). */\n spanEndBeat?: number;\n}\n\n/** Per expanded-measure-index list of holds, sorted by `atBeat`. */\nexport type HoldSchedule = MeasureHold[][];\n\n/** Default grand-pause / caesura length, in quarter-note beats. */\nconst CAESURA_BEATS = 1.0;\n/** A `short` caesura is a briefer breath. */\nconst CAESURA_SHORT_BEATS = 0.5;\n\n/**\n * Map a fermata's notated `duration` hint to a hold multiplier (how many times\n * the note's own length it sounds). `normal`/`auto`/unset \u2248 doubles the note.\n */\nfunction fermataMultiplier(fermata: Fermata | undefined): number {\n switch (fermata?.duration) {\n case \"none\":\n return 1.0;\n case \"veryShort\":\n return 1.25;\n case \"short\":\n return 1.5;\n case \"long\":\n return 2.5;\n case \"veryLong\":\n return 3.0;\n case \"normal\":\n case \"auto\":\n default:\n return 2.0;\n }\n}\n\n/** Caesura length in beats for a given style. */\nfunction caesuraBeats(style: string | undefined): number {\n return style === \"short\" ? CAESURA_SHORT_BEATS : CAESURA_BEATS;\n}\n\n/** Quarter-note beats for a Duration (base + dots). Local copy to avoid a\n * circular import with the timeline module. */\nfunction durationBeats(d: Duration): number {\n const base = DURATION_BEATS[d.base] ?? 1;\n if (!d.dots) return base;\n return base * (2 - Math.pow(2, -d.dots));\n}\n\n/** Resolve the active time signature at a global measure index. */\nfunction resolveTimeSig(globalMeasures: readonly GlobalMeasure[], measureIdx: number): TimeSignature {\n for (let m = measureIdx; m >= 0; m--) {\n const t = globalMeasures[m]!.time;\n if (t) return t;\n }\n return { count: 4, unit: 4 };\n}\n\n/** Quarter-note beats in a measure from its time signature. */\nfunction measureBeats(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\n/** One fermata'd note's time span within a measure, in quarter-note beats. */\ninterface FermataSpan {\n /** Onset beat (cursor position where the carrier starts). */\n onset: number;\n /** End beat (onset + real duration). */\n end: number;\n /** Real (tuplet-scaled) duration in beats. */\n duration: number;\n /** Hold multiplier from this note's fermata duration hint. */\n multiplier: number;\n}\n\n/**\n * Recursively collect fermata spans and caesuras from a content array,\n * advancing a beat cursor. `ratio` is the cumulative tuplet scaling factor\n * (1 at the top level; for a triplet of 3 eighths in the space of 2 it is 2/3),\n * so a fermata on a note INSIDE a tuplet reports its REAL sounded duration \u2014\n * e.g. a triplet eighth is ~0.33 beats, not 0.5. This is essential: the clarinet\n * solo before reh. 1 carries its fermata on a triplet eighth, and without\n * recursion the scanner only saw the half/whole-note fermatas and over-held the\n * whole bar. Returns the real beats consumed by `content`.\n */\nfunction collectFromContent(\n content: readonly SequenceContent[],\n startBeat: number,\n ratio: number,\n spans: FermataSpan[],\n caesuraByBeat: Map<number, number>,\n): number {\n let cursor = startBeat;\n for (const item of content) {\n if (item.type === \"event\") {\n const dur = durationBeats(item.duration) * ratio;\n const endBeat = cursor + dur;\n if (item.fermata && item.fermata.duration !== \"none\") {\n const mult = fermataMultiplier(item.fermata);\n if (dur > 0 && mult > 1) {\n spans.push({ onset: cursor, end: endBeat, duration: dur, multiplier: mult });\n }\n }\n if (item.markings?.caesura) {\n mergeMax(caesuraByBeat, endBeat, caesuraBeats(item.markings.caesura.style));\n }\n cursor = endBeat;\n } else if (item.type === \"tuplet\") {\n const outerBeats = durationBeats(item.outer.duration as Duration) * item.outer.multiple;\n const innerBeats = durationBeats(item.inner.duration as Duration) * item.inner.multiple;\n const childRatio = innerBeats > 0 ? (outerBeats / innerBeats) * ratio : ratio;\n collectFromContent(item.content, cursor, childRatio, spans, caesuraByBeat);\n cursor += outerBeats * ratio;\n } else if (item.type === \"tremolo\") {\n // Tremolo content carries no meaningful per-note fermata; just advance.\n cursor += durationBeats(item.outer.duration as Duration) * item.outer.multiple * ratio;\n } else if (item.type === \"space\") {\n cursor += (item.duration[0] / item.duration[1]) * 4 * ratio;\n }\n // grace: consumes no main-cursor time; never carries an ensemble fermata.\n }\n return cursor - startBeat;\n}\n\n/**\n * Scan one part's measure, collecting its fermata'd-note spans into `spans`\n * and folding its caesuras into `caesuraByBeat` (max-merged per beat). Recurses\n * into tuplets so nested fermatas report their real (scaled) duration.\n *\n * Spans are grouped by overlap LATER (across all parts) in `groupFermataSpans`,\n * so this just gathers the raw intervals \u2014 it does not unify here.\n */\nfunction collectPartMeasureHolds(\n partMeasure: Score[\"parts\"][number][\"measures\"][number] | undefined,\n originalMeasureIndex: number,\n staffMeterTable: ReturnType<typeof resolvePartStaffMeterTable>,\n spans: FermataSpan[],\n caesuraByBeat: Map<number, number>,\n): void {\n if (!partMeasure) return;\n for (const seq of partMeasure.sequences) {\n if (seq.fullMeasure) continue;\n const ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, seq.staff ?? 1);\n collectFromContent(seq.content, 0, ratio, spans, caesuraByBeat);\n }\n}\n\n/** Floating-point slop for beat comparisons. */\nconst BEAT_EPS = 1e-9;\n\n/**\n * Group fermata spans by overlapping intervals and emit one hold per group.\n *\n * Spans are sorted by onset; a span that starts before the running group's end\n * (strictly \u2014 touching spans do NOT merge, so back-to-back fermatas stay\n * separate holds) extends the group. Each group's hold amount is\n * `(mult - 1) * minDuration` where `minDuration` is the shortest span in the\n * group (ties broken toward the larger multiplier), inserted at that DRIVING\n * note's END beat so the hold lands in place at the short note. The group's\n * full [startBeat, spanEnd) is recorded so fermata carriers can be matched.\n */\nfunction groupFermataSpans(spans: FermataSpan[]): MeasureHold[] {\n if (spans.length === 0) return [];\n spans.sort((a, b) => a.onset - b.onset || a.end - b.end);\n\n const holds: MeasureHold[] = [];\n let groupStart = spans[0]!.onset;\n let groupEnd = spans[0]!.end;\n let minDur = spans[0]!.duration;\n let mult = spans[0]!.multiplier;\n let driverEnd = spans[0]!.end; // END of the shortest span = insertion point\n\n const flush = (): void => {\n const extraBeats = (mult - 1) * minDur;\n if (extraBeats > BEAT_EPS) {\n holds.push({ atBeat: driverEnd, startBeat: groupStart, spanEndBeat: groupEnd, extraBeats, kind: \"fermata\" });\n }\n };\n\n for (let i = 1; i < spans.length; i++) {\n const s = spans[i]!;\n if (s.onset < groupEnd - BEAT_EPS) {\n // Overlaps the current group \u2192 extend it.\n groupEnd = Math.max(groupEnd, s.end);\n if (s.duration < minDur - BEAT_EPS) {\n minDur = s.duration;\n mult = s.multiplier;\n driverEnd = s.end;\n } else if (Math.abs(s.duration - minDur) < BEAT_EPS && s.multiplier > mult) {\n mult = s.multiplier;\n }\n } else {\n // Disjoint \u2192 close the current group and start a new one.\n flush();\n groupStart = s.onset;\n groupEnd = s.end;\n minDur = s.duration;\n mult = s.multiplier;\n driverEnd = s.end;\n }\n }\n flush();\n return holds;\n}\n\n/** Insert or max-merge a value at a beat position. */\nfunction mergeMax(byBeat: Map<number, number>, atBeat: number, value: number): void {\n const prev = byBeat.get(atBeat) ?? 0;\n if (value > prev) byBeat.set(atBeat, value);\n}\n\n/**\n * Build the per-measure hold schedule for the expanded measure order.\n *\n * @param score The score (for part measures).\n * @param measureOrder Expanded measure indices (post repeat/jump expansion).\n * @param globalMeasures Original global measures (for caesura + time sig).\n * @returns One sorted `MeasureHold[]` per expanded measure index.\n */\nexport function buildHoldSchedule(\n score: Score,\n measureOrder: readonly number[],\n globalMeasures: readonly GlobalMeasure[],\n): HoldSchedule {\n const schedule: HoldSchedule = [];\n const staffMeterTables = score.parts.map((part) => resolvePartStaffMeterTable(part, globalMeasures));\n\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const origIdx = measureOrder[expandedIdx]!;\n const fermataSpans: FermataSpan[] = [];\n const caesuraByBeat = new Map<number, number>();\n\n for (let partIndex = 0; partIndex < score.parts.length; partIndex++) {\n const part = score.parts[partIndex]!;\n collectPartMeasureHolds(\n part.measures[origIdx],\n origIdx,\n staffMeterTables[partIndex]!,\n fermataSpans,\n caesuraByBeat,\n );\n }\n\n // Global caesura on the measure \u2192 grand pause at the measure end.\n const globalCaesura = globalMeasures[origIdx]?.caesura;\n if (globalCaesura) {\n const mBeats = measureBeats(resolveTimeSig(globalMeasures, origIdx));\n mergeMax(caesuraByBeat, mBeats, caesuraBeats(globalCaesura.style));\n }\n\n // Fermatas: group by overlapping spans \u2192 one hold per group (see\n // groupFermataSpans). Non-overlapping fermatas yield distinct holds.\n const holds: MeasureHold[] = groupFermataSpans(fermataSpans);\n for (const [atBeat, extraBeats] of caesuraByBeat) {\n holds.push({ atBeat, extraBeats, kind: \"caesura\" });\n }\n holds.sort((a, b) => a.atBeat - b.atBeat);\n schedule.push(holds);\n }\n\n return schedule;\n}\n", "import {\n ratioToNumber,\n resolveStaffMeterTimeline,\n type EffectiveStaffMeter,\n type GlobalMeasure,\n type Part,\n type TimeSignature,\n} from \"@viritura/core\";\n\nexport type StaffMeterTable = readonly ReadonlyMap<number, EffectiveStaffMeter>[];\n\n/** Resolve the active global time signature at a measure index. */\nexport function resolveActiveTime(globalMeasures: readonly GlobalMeasure[], measureIndex: number): TimeSignature {\n for (let index = measureIndex; index >= 0; index--) {\n const time = globalMeasures[index]?.time;\n if (time) return time;\n }\n return { count: 4, unit: 4 };\n}\n\n/** Resolve every effective staff-local meter for one part. */\nexport function resolvePartStaffMeterTable(part: Part, globalMeasures: readonly GlobalMeasure[]): StaffMeterTable {\n const globalTimeAt = globalMeasures.map((_, index) => resolveActiveTime(globalMeasures, index));\n return resolveStaffMeterTimeline(\n part.measures.map((measure) => measure.staffMeters),\n globalTimeAt,\n ).table;\n}\n\n/** Scale written beats on a staff onto the shared global-measure axis. */\nexport function staffMeterRatioAt(table: StaffMeterTable, measureIndex: number, staff: number | undefined): number {\n if (staff === undefined) return 1;\n const effective = table[measureIndex]?.get(staff);\n return effective ? ratioToNumber(effective.ratioToGlobal) : 1;\n}\n", "import {\n DURATION_BEATS,\n type GlobalMeasure,\n type Score,\n type SequenceContent,\n type TimeSignature,\n} from \"@viritura/core\";\nimport type { HoldSchedule } from \"./holds\";\n\ntype PlaybackGlobalMeasure = GlobalMeasure & { __playbackNominalTime?: TimeSignature };\n\nfunction contentBeats(content: readonly SequenceContent[]): number {\n let beats = 0;\n for (const item of content) {\n switch (item.type) {\n case \"event\":\n beats += durationBeats(item.duration.base, item.duration.dots);\n break;\n case \"space\":\n beats += (item.duration[0] / item.duration[1]) * 4;\n break;\n case \"tuplet\":\n case \"tremolo\":\n beats += durationBeats(item.outer.duration.base, item.outer.duration.dots) * item.outer.multiple;\n break;\n case \"grace\":\n break;\n }\n }\n return beats;\n}\n\nfunction durationBeats(base: keyof typeof DURATION_BEATS, dots?: number): number {\n const beats = DURATION_BEATS[base];\n return dots ? beats * (2 - Math.pow(2, -dots)) : beats;\n}\n\nfunction writtenMeasureBeats(score: Score, measureIndex: number, minimumBeats: number): number {\n return score.parts.reduce((maxBeats, part) => {\n const measure = part.measures[measureIndex];\n if (!measure) return maxBeats;\n return Math.max(\n maxBeats,\n ...measure.sequences\n .filter((sequence) => !sequence.fullMeasure)\n .map((sequence) => contentBeats(sequence.content)),\n );\n }, minimumBeats);\n}\n\n/**\n * Playback-only global measures for pickups and open-meter cadenzas. The\n * displayed time signature remains untouched; timing spans the longest written\n * sequence so every part advances together when the shortened measure ends.\n */\nexport function playbackGlobalMeasures(score: Score, measureOrder: readonly number[]): GlobalMeasure[] {\n let activeBeats = 4;\n return measureOrder.map((measureIndex) => {\n const measure = score.global.measures[measureIndex]!;\n if (measure.time) activeBeats = (measure.time.count * 4) / measure.time.unit;\n if (measureIndex === 0 && measure.number === 0) {\n const pickupBeats = writtenMeasureBeats(score, measureIndex, 0);\n if (pickupBeats > 0) {\n return {\n ...measure,\n __playbackNominalTime: measure.time ? { ...measure.time } : undefined,\n time: {\n ...measure.time,\n count: pickupBeats,\n unit: 4,\n },\n } as PlaybackGlobalMeasure;\n }\n }\n if (measure.time?.display !== \"senzaMisura\") return measure;\n\n const cadenzaBeats = writtenMeasureBeats(score, measureIndex, activeBeats);\n return {\n ...measure,\n time: {\n ...measure.time,\n count: cadenzaBeats,\n unit: 4,\n },\n };\n });\n}\n\n/** Visual fermatas in an open-meter cadenza do not introduce a second delay. */\nexport function suppressCadenzaFermataHolds(measures: readonly GlobalMeasure[], holds: HoldSchedule): HoldSchedule {\n return holds.map((measureHolds, index) =>\n measures[index]?.time?.display === \"senzaMisura\"\n ? measureHolds.filter((hold) => hold.kind !== \"fermata\")\n : [...measureHolds],\n );\n}\n", "/**\n * Slur legato \u2014 connects notes under a slur by overlapping their releases.\n *\n * Splits into two phases:\n * 1. A pre-scan (`collectPartLegatoOut`) marks every event id that is interior\n * to a slur span (covered by a slur and followed by another note still\n * under it). The slur's final note is excluded \u2014 it takes a normal release.\n * 2. At emit time the timeline defers an interior note's `noteOff` and resolves\n * it against the next note's onset (`resolveLegato`), or flushes it at its\n * natural time when no next note follows (`flushLegatoNatural`).\n */\n\nimport type { Note, NoteEvent, Score, SequenceContent } from \"@viritura/core\";\nimport { pitchToMidi } from \"@viritura/core\";\nimport type { MidiEvent } from \"./types\";\nimport type { TempoModel } from \"./tempoModel\";\nimport { timingHumanize } from \"./dynamics\";\n\n/**\n * Legato overlap for slurred notes, in seconds. An interior slur note's release\n * is pushed just past the next note's onset so the two voices briefly overlap \u2014\n * the next attack masks the previous release, reading as a connected (legato)\n * line rather than re-articulated d\u00E9tach\u00E9 notes. Standard performance practice:\n * notes under a slur are played without separation. The overlap is capped to\n * the next note's own duration (see `resolveLegato`) so the overhang can never\n * reach the note after it.\n */\nconst LEGATO_OVERLAP_SEC = 0.02;\n\n/**\n * Flatten a voice's content into an ordered list of note events, recursing into\n * tuplets and unwrapping grace/tremolo containers. The order matches the\n * notated reading order, which is what \"interior to a slur span\" is defined\n * against.\n */\nfunction collectVoiceEvents(content: readonly SequenceContent[], out: NoteEvent[]): void {\n for (const item of content) {\n if (item.type === \"event\") {\n out.push(item);\n } else if (item.type === \"tuplet\") {\n collectVoiceEvents(item.content, out);\n } else if (item.type === \"grace\") {\n for (const evt of item.content) out.push(evt);\n } else if (item.type === \"tremolo\") {\n for (const evt of item.content) out.push(evt);\n }\n }\n}\n\n/**\n * Pre-scan a part to collect every event id that is *interior* to a slur span \u2014\n * i.e. covered by a slur and followed by another note still under that slur.\n * These events are played legato: their release overlaps the next note's onset.\n * The slur's final note is excluded (it takes a normal, articulated release).\n *\n * Events are grouped by voice (so a slur only connects notes within its own\n * voice) and scanned in score order. Repeats are not expanded: ids are unique\n * in the source, and an interior id stays interior on every repeat pass.\n */\nexport function collectPartLegatoOut(part: Score[\"parts\"][number]): Set<string> {\n const voices = new Map<string, NoteEvent[]>();\n for (const partMeasure of part.measures) {\n if (!partMeasure) continue;\n partMeasure.sequences.forEach((seq, seqIdx) => {\n const voiceKey = seq.voice ?? `#${seqIdx}`;\n let list = voices.get(voiceKey);\n if (!list) {\n list = [];\n voices.set(voiceKey, list);\n }\n collectVoiceEvents(seq.content, list);\n });\n }\n\n const legatoOut = new Set<string>();\n for (const events of voices.values()) {\n const indexById = new Map<string, number>();\n events.forEach((evt, i) => {\n if (evt.id) indexById.set(evt.id, i);\n });\n events.forEach((evt, startIdx) => {\n if (!evt.slurs) return;\n for (const slur of evt.slurs) {\n const endIdx = slur.target ? indexById.get(slur.target) : undefined;\n if (endIdx === undefined || endIdx <= startIdx) continue;\n // Mark every event from the slur start up to (not including) its end.\n for (let i = startIdx; i < endIdx; i++) {\n const id = events[i]!.id;\n if (id) legatoOut.add(id);\n }\n }\n });\n }\n return legatoOut;\n}\n\n/** Collect the in-range MIDI notes of an event into a set. */\nfunction midiNotesOf(notes: readonly Note[]): Set<number> {\n const set = new Set<number>();\n for (const note of notes) {\n const m = pitchToMidi(note.pitch);\n if (m >= 0 && m <= 127) set.add(m);\n }\n return set;\n}\n\n/**\n * Resolve all deferred legato noteOffs against the next note's onset.\n *\n * - Distinct pitch: release overlaps the next attack by `LEGATO_OVERLAP_SEC`,\n * capped to the next note's own duration so the overhang ends within that\n * note and never reaches the note after it.\n * - Repeated pitch (same MIDI note): no overlap \u2014 release exactly at the next\n * onset. Overlapping a note with itself would steal/cut the voice; a gapless\n * release-then-attack instead re-articulates the repeated note cleanly.\n */\nfunction resolveLegato(\n pendingLegato: MidiEvent[],\n out: MidiEvent[],\n nextOnset: number,\n nextDurationSec: number,\n nextNotes: readonly Note[],\n): void {\n const nextMidi = midiNotesOf(nextNotes);\n const overlap = Math.min(LEGATO_OVERLAP_SEC, Math.max(0, nextDurationSec));\n for (const off of pendingLegato) {\n off.time = nextMidi.has(off.midiNote) ? nextOnset : nextOnset + overlap;\n out.push(off);\n }\n pendingLegato.length = 0;\n}\n\n/**\n * Flush deferred legato noteOffs at their natural (un-extended) time. Used when\n * the slur runs into a rest or the end of the part \u2014 there is no next note to\n * overlap, so the note releases as written.\n */\nfunction flushLegatoNatural(pendingLegato: MidiEvent[], out: MidiEvent[]): void {\n for (const off of pendingLegato) out.push(off);\n pendingLegato.length = 0;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Per-voice deferral buckets\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n//\n// A part's deferred legato offs are bucketed BY VOICE so a slur in one voice\n// isn't resolved (or stolen) by a note in another \u2014 critical for multi-voice\n// parts like piano where two hands share pitches.\n\n/** Append a deferred legato off to its voice's bucket (created on demand). */\nexport function pushVoiceLegato(map: Map<string, MidiEvent[]>, voiceKey: string, off: MidiEvent): void {\n const bucket = map.get(voiceKey);\n if (bucket) bucket.push(off);\n else map.set(voiceKey, [off]);\n}\n\n/** Flush one voice's deferred legato offs at their natural time (rest boundary). */\nexport function flushVoiceLegato(map: Map<string, MidiEvent[]>, voiceKey: string, out: MidiEvent[]): void {\n const bucket = map.get(voiceKey);\n if (bucket) flushLegatoNatural(bucket, out);\n}\n\n/** Flush every voice's remaining deferred legato offs (end of part). */\nexport function flushAllLegato(map: Map<string, MidiEvent[]>, out: MidiEvent[]): void {\n for (const bucket of map.values()) flushLegatoNatural(bucket, out);\n}\n\n/** Args for `resolveVoiceLegato` \u2014 the current voice's deferred offs against the next onset. */\nexport interface ResolveVoiceLegatoArgs {\n map: Map<string, MidiEvent[]>;\n voiceKey: string;\n out: MidiEvent[];\n /** Continuous tempo model (timing source). */\n model: TempoModel;\n /** Global beat at the current measure's start. */\n measureStartBeat: number;\n partIndex: number;\n /** Measure-start time (seconds) \u2014 used only as the deterministic jitter seed. */\n measureStartTime: number;\n beatOffset: number;\n beats: number;\n notes: readonly Note[];\n}\n\n/**\n * Resolve the current voice's deferred legato offs against the event at\n * (`measureStartBeat + beatOffset`). No-op when that voice has nothing pending.\n * The onset uses the same jittered formula as the noteOn so the overlap is exact.\n */\nexport function resolveVoiceLegato(a: ResolveVoiceLegatoArgs): void {\n const bucket = a.map.get(a.voiceKey);\n if (!bucket || bucket.length === 0) return;\n const onset = Math.max(\n 0,\n a.model.timeAtBeat(a.measureStartBeat + a.beatOffset) +\n timingHumanize(a.measureStartTime, a.beatOffset, a.partIndex),\n );\n const nextDur = a.model.secondsForBeats(a.measureStartBeat + a.beatOffset, a.beats);\n resolveLegato(bucket, a.out, onset, nextDur, a.notes);\n}\n", "/**\n * Articulation-based velocity shaping and metric/humanize offsets.\n *\n * The dynamic level \u2192 {velocity, CC11} mapping lives in `dynamicsEnvelope.ts`\n * (the coupled-dynamics system). This module holds the per-note attack-character\n * adjustments layered on top: accents, articulation duration scaling, metric\n * accent, and deterministic velocity/timing humanization.\n */\n\nimport type { Markings, TimeSignature } from \"@viritura/core\";\n\n/**\n * Apply articulation-based velocity adjustments.\n *\n * - accent: +20 (capped at 127)\n * - strongAccent (marcato): +30 (capped at 127)\n * - stress: +15\n * - unstress: \u221215 (floored at 1)\n * - softAccent: +10\n */\nexport function applyArticulationVelocity(baseVelocity: number, markings: Markings | undefined): number {\n if (!markings) return baseVelocity;\n\n let v = baseVelocity;\n if (markings.accent) v += 20;\n if (markings.strongAccent) v += 30;\n if (markings.stress) v += 15;\n if (markings.unstress) v -= 15;\n if (markings.softAccent) v += 10;\n\n return Math.max(1, Math.min(127, v));\n}\n\n/**\n * Compute the duration scaling factor for articulation markings.\n *\n * - staccato: 50% of notated duration\n * - staccatissimo: 25% of notated duration\n * - spiccato: 30% of notated duration\n * - tenuto: 100% of notated duration\n * - default (no marking): 90% (slight separation between notes)\n *\n * standard engraving practice defaults \u2014 staccato=50%, staccatissimo=33%,\n * tenuto=100%, default=~90%.\n */\nexport function articulationDurationScale(markings: Markings | undefined): number {\n if (!markings) return 0.9;\n if (markings.staccatissimo) return 0.25;\n if (markings.spiccato) return 0.3;\n if (markings.staccato) return 0.5;\n if (markings.tenuto) return 1.0;\n return 0.9;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Metric accent \u2014 subtle velocity shaping by beat position\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Maximum velocity offset for metric accent (applied at the strongest beat).\n * Kept small to remain subtle: \u00B16 on a 0\u2013127 scale (~5%).\n */\nconst METRIC_ACCENT_RANGE = 6;\n\n/**\n * Compute a metric accent velocity offset for a given beat position.\n *\n * Uses hierarchical subdivision weighting: each time you halve the beat grid,\n * positions that fall on the finer grid get progressively weaker. The result\n * is a signed offset (positive = stronger, negative = weaker) that should be\n * added to the base velocity.\n *\n * For simple meters (2/4, 3/4, 4/4, 2/2, etc.) this produces the natural\n * strong/weak pattern:\n * 4/4: beat 1 strongest, 3 strong, 2 & 4 weak; 8ths alternate, 16ths finer.\n *\n * **Future work (compound / asymmetric meters):**\n * This function currently assumes beats are grouped in powers of 2 from the\n * measure downbeat. For compound meters like 6/8 (2 groups of 3 eighth notes)\n * or 12/8 (4 groups of 3), the strong-beat hierarchy is different \u2014 e.g. in\n * 6/8, beat 4 (the dotted-quarter group boundary) should be nearly as strong\n * as beat 1, not weak. Similarly, asymmetric meters like 5/8 (2+3 or 3+2) or\n * 7/8 (2+2+3) have user-defined beat groupings.\n *\n * Once Viritura supports user-configurable beat/beam grouping per time\n * signature (similar to Dorico's beat grouping editor for complex meters),\n * this function should accept the beat group boundaries and compute metric\n * weight relative to each group's downbeat rather than the measure downbeat.\n * The grouping data would define which beats are \"strong\" within each\n * subdivision group, enabling correct metric accenting for any meter.\n *\n * @param beatOffset - Beat position within the measure (in quarter-note beats, 0-based)\n * @param timeSignature - Active time signature\n * @returns Signed velocity offset to add to base velocity (clamped later)\n */\nexport function metricAccentOffset(beatOffset: number, timeSignature: TimeSignature): number {\n const measureBeats = (timeSignature.count / timeSignature.unit) * 4;\n if (measureBeats <= 0) return 0;\n\n // Normalize position to [0, measureBeats)\n const pos = ((beatOffset % measureBeats) + measureBeats) % measureBeats;\n\n // Compute metric weight by checking which subdivision level this beat falls on.\n // Level 0 = downbeat (pos \u2248 0), highest weight.\n // Level 1 = falls on half-measure grid but not downbeat.\n // Level 2 = falls on quarter-measure grid but not half, etc.\n // Weight decreases by 1 per level. Maximum depth = 5 (down to 64th-note grid).\n const MAX_DEPTH = 5;\n let level = MAX_DEPTH; // default: finest subdivision\n\n // Check from coarsest to finest grid\n let grid = measureBeats; // full measure\n for (let d = 0; d <= MAX_DEPTH; d++) {\n // Check if pos falls on this grid (within tolerance for floating-point)\n const remainder = pos % grid;\n if (remainder < 1e-6 || grid - remainder < 1e-6) {\n level = d;\n break;\n }\n grid /= 2;\n }\n\n // Map level to velocity offset:\n // level 0 (downbeat) \u2192 +RANGE\n // level MAX_DEPTH (finest) \u2192 -RANGE\n // Linear interpolation between them\n const normalized = level / MAX_DEPTH; // 0 = strongest, 1 = weakest\n return Math.round(METRIC_ACCENT_RANGE * (1 - 2 * normalized));\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Velocity humanization \u2014 deterministic per-note jitter\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/**\n * Maximum velocity jitter for humanization.\n *\n * Kept at \u00B12 to preserve the metric accent hierarchy:\n * Metric accent offsets: +6, +4, +1, -1, -4, -6 (levels 0\u20135)\n * Minimum gap between non-adjacent levels: 5 (downbeat +6 vs quarter +1)\n * With \u00B12 jitter, downbeat worst-case (+4) > quarter best-case (+3) \u2713\n *\n * Only adjacent subdivision levels (e.g. downbeat vs half-measure) can\n * occasionally overlap, which is musically natural \u2014 beat 3 in 4/4 is\n * nearly as strong as beat 1 in real performance.\n */\nconst HUMANIZE_MAX = 2;\n\n/**\n * Compute a deterministic velocity jitter for a note at a given position.\n *\n * Uses a hash of the measure's absolute time and the beat offset to produce\n * a consistent per-note offset. The same passage always sounds the same on\n * replay, but different notes within and across measures get different jitter\n * values, eliminating the \"machine gun\" effect of identical velocities.\n *\n * @param measureStartTime - Absolute start time of the measure in seconds\n * @param beatOffset - Beat position within the measure (quarter-note beats)\n * @returns Integer velocity offset in [-HUMANIZE_MAX, +HUMANIZE_MAX]\n */\nexport function velocityHumanize(measureStartTime: number, beatOffset: number): number {\n // Deterministic pseudo-random hash: sin(x) * large_prime produces chaotic\n // fractional parts, giving a well-distributed [0,1) value per input pair.\n // This is a standard technique from GLSL shader noise functions.\n const x = measureStartTime * 12.9898 + beatOffset * 78.233;\n const hash = Math.abs(Math.sin(x) * 43758.5453) % 1.0;\n return Math.round(hash * 2 * HUMANIZE_MAX - HUMANIZE_MAX);\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Timing humanization\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Maximum timing jitter in seconds (\u00B115 ms). */\nconst TIMING_JITTER_MAX = 0.015;\n\n/**\n * Deterministic per-note onset timing jitter.\n *\n * Real players never attack at exactly the same instant. This adds a small\n * random offset (\u00B115 ms) to each note's onset time based on a hash of the\n * measure time, beat position, and part index. Using different magic\n * constants from velocityHumanize ensures timing and velocity jitter are\n * uncorrelated.\n *\n * The part index is included so that two parts playing the same rhythm get\n * different timing offsets, which is essential for realism in unisons.\n *\n * @param measureStartTime - Absolute start time of the measure in seconds\n * @param beatOffset - Beat position within the measure (quarter-note beats)\n * @param partIndex - Part index (so unison parts get different offsets)\n * @returns Timing offset in seconds in [-TIMING_JITTER_MAX, +TIMING_JITTER_MAX]\n */\nexport function timingHumanize(measureStartTime: number, beatOffset: number, partIndex: number): number {\n // Different constants from velocityHumanize to decorrelate timing and velocity.\n const x = measureStartTime * 17.1391 + beatOffset * 53.7829 + partIndex * 91.4173;\n const hash = Math.abs(Math.sin(x) * 29587.7139) % 1.0;\n return hash * 2 * TIMING_JITTER_MAX - TIMING_JITTER_MAX;\n}\n", "/**\n * Playing-technique keyswitching for the MIDI timeline \u2014 pizz/arco bowing and\n * con sord./open muting, applied per measure as persistent state.\n *\n * Split out of `timeline.ts` (it crossed the 800-line file limit). This is the\n * self-contained \"performance technique\" sub-concept: it parses technique text\n * directions and emits the corresponding `programChange` / `controlChange`\n * events into the part's output buffer. The per-part traversal context\n * (`PartCtx`) and the `fractionToBeats` helper are shared back from\n * `timeline.ts`; `beatOffsetToTime` comes from the tempo map.\n */\nimport type { TextExpression } from \"@viritura/core\";\nimport { type PartCtx, fractionToBeats } from \"./timeline\";\n\n/** GM program 45 \u2014 Pizzicato Strings. */\nconst GM_PIZZICATO_STRINGS = 45;\n\n/**\n * Baseline GM programs for which pizz/arco keyswitching is meaningful:\n * solo strings (40\u201343) and string ensembles (48\u201349). On any other baseline\n * a \"pizz.\"/\"arco\" text direction is ignored for playback.\n */\nexport const ARCO_CAPABLE_PROGRAMS = new Set([40, 41, 42, 43, 48, 49]);\n\n/** Instrument family for mute (con sord.) handling, derived from GM program. */\nexport type MuteFamily = \"strings\" | \"brass\";\n\n/**\n * Classify a GM program into a mute-capable family. Strings (40\u201351, incl.\n * solo + ensemble + synth strings) and brass (56\u201363). Winds and everything\n * else return null \u2014 a \"con sord.\" direction there is ignored for playback.\n */\nexport function muteFamilyForProgram(program: number): MuteFamily | null {\n if (program >= 40 && program <= 51) return \"strings\";\n if (program >= 56 && program <= 63) return \"brass\";\n return null;\n}\n\n// MIDI controllers used to approximate a mute via the synth's per-voice filter.\n// These are per-channel (= per-part), applied inside the shared synth, so no\n// audio-graph rework is needed. A mute is TIMBRE only here (CC 74/71); it does\n// NOT touch level. CC 7 (channel volume) belongs to the mixer, and CC 11\n// (expression) is owned exclusively by the coupled dynamics system.\n/** CC 74 \u2014 Brightness (low-pass filter cutoff; 64 = neutral, lower = darker). */\nconst CC_BRIGHTNESS = 74;\n/** CC 71 \u2014 Harmonic Intensity (filter resonance; 64 = neutral). */\nconst CC_RESONANCE = 71;\n\n/** Filter curve applied to a channel for a given mute state (timbre only). */\ninterface MuteCurve {\n brightness: number;\n resonance: number;\n}\n\n/** Per-family muted curves and the shared \"open\" (restored) curve. */\nconst MUTE_CURVES: Record<MuteFamily, MuteCurve> = {\n // con sordino: soft, veiled \u2014 roll off brilliance, flat resonance.\n strings: { brightness: 44, resonance: 70 },\n // straight/Harmon mute: pinched and nasal, NOT dark. A muted trumpet keeps\n // (even emphasizes) its upper harmonics, so the cutoff stays near neutral \u2014\n // a hard lowpass here silences high brass (the trumpet sits above the cutoff).\n // The nasal color comes mostly from resonance.\n brass: { brightness: 58, resonance: 84 },\n};\n/** Neutral curve restored on \"senza sord.\" / \"open\". */\nconst OPEN_CURVE: MuteCurve = { brightness: 64, resonance: 64 };\n\n/** A persistent technique direction parsed from an expression. */\nexport type TechniqueAction = { kind: \"bow\"; pizz: boolean } | { kind: \"mute\"; muted: boolean };\n\n/** Normalized words that select pizzicato. */\nconst PIZZ_WORDS = new Set([\"pizz\", \"pizzicato\"]);\n/** Normalized words that return to ordinary (arco) bowing. */\nconst ARCO_WORDS = new Set([\"arco\", \"ord\", \"ordinario\", \"naturale\", \"nat\"]);\n/** Normalized words that engage a mute (con sordino). */\nconst MUTE_ON_WORDS = new Set([\"consord\", \"consordino\", \"consordini\", \"muted\", \"mute\", \"sord\", \"sordino\"]);\n/** Normalized words that remove a mute (senza sordino / open). */\nconst MUTE_OFF_WORDS = new Set([\"senzasord\", \"senzasordino\", \"senzasordini\", \"open\", \"viasord\"]);\n\n/**\n * Match an expression's text against the technique vocabulary, returning the\n * persistent state change it implies (bowing or mute), or null if it isn't a\n * recognized technique. Strips punctuation/whitespace so \"pizz.\", \"(pizz.)\",\n * \"con sord.\" all match.\n */\nexport function classifyTechniqueText(text: string): TechniqueAction | null {\n const word = text.toLowerCase().replace(/[^a-z]/g, \"\");\n if (PIZZ_WORDS.has(word)) return { kind: \"bow\", pizz: true };\n if (ARCO_WORDS.has(word)) return { kind: \"bow\", pizz: false };\n if (MUTE_ON_WORDS.has(word)) return { kind: \"mute\", muted: true };\n if (MUTE_OFF_WORDS.has(word)) return { kind: \"mute\", muted: false };\n return null;\n}\n\n/** Persistent per-part technique state, threaded across measures. */\nexport interface TechniqueState {\n /** Active GM program (pizz vs the part baseline). */\n program: number;\n /** Whether the part is currently muted (con sord.). */\n muted: boolean;\n}\n\n/** Emit a controlChange event for one CC at a given time. */\nfunction emitCc(ctx: PartCtx, time: number, cc: number, value: number): void {\n ctx.out.push({\n type: \"controlChange\",\n time,\n midiNote: 0,\n velocity: 0,\n partIndex: ctx.partIndex,\n channel: ctx.channel,\n cc,\n value,\n });\n}\n\n/**\n * Keyswitch-style technique application for a single measure.\n *\n * Scans the measure's text expressions for pizz/arco and con sord./open\n * markings and emits the corresponding control events whenever a state\n * changes. The synth holds each setting until the next change, so techniques\n * persist across notes and measures \u2014 exactly like a sampler keyswitch.\n *\n * - Bowing (strings only): `pizz.` \u2192 GM Pizzicato Strings via `programChange`;\n * `arco` restores the part baseline.\n * - Mute (strings + brass): `con sord.` applies a per-family filter curve via\n * CC 74/71 (timbre only); `senza sord.`/`open` restores the neutral curve.\n *\n * Returns the updated state so the caller threads it into the next measure.\n */\nexport function applyMeasureTechniques(\n expressions: readonly TextExpression[] | undefined,\n ctx: PartCtx,\n state: TechniqueState,\n caps: { bow: boolean; mute: MuteFamily | null },\n ratioForStaff: (staff: number | undefined) => number = () => 1,\n): TechniqueState {\n if (!expressions || expressions.length === 0) return state;\n\n // Resolve technique markings and sort by beat so transitions within the\n // measure resolve in playing order.\n const marks = expressions\n .map((e) => ({\n action: classifyTechniqueText(e.text),\n beat: fractionToBeats(e.position.fraction) * ratioForStaff(e.staff),\n }))\n .filter((m): m is { action: TechniqueAction; beat: number } => m.action !== null)\n .sort((a, b) => a.beat - b.beat);\n\n let { program, muted } = state;\n for (const mark of marks) {\n const time = ctx.model.timeAtBeat(ctx.measureStartBeat + mark.beat);\n if (mark.action.kind === \"bow\" && caps.bow) {\n const target = mark.action.pizz ? GM_PIZZICATO_STRINGS : ctx.gmProgram;\n if (target === program) continue;\n program = target;\n // The global sort orders this programChange before any noteOn at the same time.\n ctx.out.push({\n type: \"programChange\",\n time,\n midiNote: 0,\n velocity: 0,\n partIndex: ctx.partIndex,\n channel: ctx.channel,\n program: target,\n });\n } else if (mark.action.kind === \"mute\" && caps.mute) {\n if (mark.action.muted === muted) continue;\n muted = mark.action.muted;\n const curve = muted ? MUTE_CURVES[caps.mute] : OPEN_CURVE;\n emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness);\n emitCc(ctx, time, CC_RESONANCE, curve.resonance);\n }\n }\n return { program, muted };\n}\n", "/**\n * Tempo map builder \u2014 walks global measures and produces a time-indexed\n * list of tempo changes for converting beat positions to absolute seconds.\n *\n * All BPM values stored in the tempo map are normalized to effective\n * quarter-note BPM. For example, \"dotted quarter = 120\" becomes\n * effective QPM = 120 \u00D7 1.5 = 180.\n */\n\nimport type { GlobalMeasure, TimeSignature } from \"@viritura/core\";\nimport { DURATION_BEATS } from \"@viritura/core\";\nimport type { TempoMapEntry } from \"./types\";\nimport type { HoldSchedule } from \"./holds\";\nimport { TempoModel, type TempoRegion, type TempoInsertion } from \"./tempoModel\";\n\ntype PlaybackGlobalMeasure = GlobalMeasure & { __playbackNominalTime?: TimeSignature };\n\nfunction playbackNominalTime(gm: GlobalMeasure): TimeSignature | undefined {\n return (gm as PlaybackGlobalMeasure).__playbackNominalTime;\n}\n\n/** Default tempo when no tempo marking is specified. */\nexport const DEFAULT_BPM = 120;\n\n/** Default time signature when none is specified. */\nconst DEFAULT_TIME: TimeSignature = { count: 4, unit: 4 };\n\n/**\n * Compute beats in a measure from its time signature.\n * Returns the number of quarter-note beats.\n */\nexport function measureBeatsFromTime(ts: TimeSignature): number {\n return (ts.count * 4) / ts.unit;\n}\n\n/**\n * Compute the number of quarter-note beats for a tempo's note value.\n * A \"dotted quarter\" = 1.5 quarter-note beats.\n */\nexport function tempoNoteBeats(base: string, dots?: number): number {\n const baseBeats = DURATION_BEATS[base as keyof typeof DURATION_BEATS] ?? 1;\n if (!dots) return baseBeats;\n // Dot formula: total = base \u00D7 (2 \u2212 1/2^dots)\n return baseBeats * (2 - Math.pow(2, -dots));\n}\n\n/**\n * Convert a tempo marking to effective quarter-note BPM.\n * \"dotted quarter = 120\" \u2192 120 \u00D7 1.5 = 180 effective QPM.\n */\nexport function effectiveQpm(bpm: number, tempoBase: string, tempoDots?: number): number {\n return bpm * tempoNoteBeats(tempoBase, tempoDots);\n}\n\n/**\n * Build a tempo map from the global measures of a score.\n *\n * Each entry records the absolute time at which a tempo change occurs,\n * plus the measure index and beat offset. The `bpm` field stores the\n * effective quarter-note BPM (normalized from whatever note value the\n * tempo marking references).\n *\n * The first entry always starts at time 0. If no tempo is specified,\n * the default of 120 QPM is used.\n *\n * When a `holds` schedule is supplied (one entry per expanded measure), the\n * extra time that fermatas/caesuras insert is added to the running clock at\n * the measure-start tempo, so every later measure's start time is pushed back\n * uniformly \u2014 keeping all parts aligned through the hold.\n *\n * @returns tempoMap entries and cumulative measure start times (in seconds).\n */\nexport function buildTempoMap(\n globalMeasures: readonly GlobalMeasure[],\n holds?: HoldSchedule,\n): {\n tempoMap: TempoMapEntry[];\n measureStartTimes: number[];\n} {\n const tempoMap: TempoMapEntry[] = [];\n const measureStartTimes: number[] = [];\n\n let currentQpm = DEFAULT_BPM;\n let currentSpq = 60 / currentQpm;\n let currentTime = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit };\n let absoluteTime = 0;\n let hasInitialTempo = false;\n\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n const nominalTime = playbackNominalTime(gm) ?? gm.time;\n\n if (nominalTime) {\n currentTime = { count: nominalTime.count, unit: nominalTime.unit };\n }\n\n const mBeats = measureBeatsFromTime(gm.time ?? currentTime);\n measureStartTimes.push(absoluteTime);\n\n // Seconds-per-beat at the measure start \u2014 used for hold insertion so it\n // matches the timeline's within-measure event timing (which also uses the\n // measure-start tempo).\n const measureStartSpq = currentSpq;\n\n // Collect and sort tempo changes within this measure by beat position\n interface TempoChange {\n beatOffset: number;\n qpm: number;\n }\n const changes: TempoChange[] = [];\n\n if (gm.tempos && gm.tempos.length > 0) {\n for (const tempo of gm.tempos) {\n const beatOffset = tempo.location ? (tempo.location.fraction[0] / tempo.location.fraction[1]) * mBeats : 0;\n const qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);\n changes.push({ beatOffset, qpm });\n }\n changes.sort((a, b) => a.beatOffset - b.beatOffset);\n }\n\n // Walk through the measure, advancing time segment-by-segment\n let beatCursor = 0;\n for (const change of changes) {\n // Advance time from beatCursor to the change's beat offset at the current tempo\n if (change.beatOffset > beatCursor) {\n absoluteTime += (change.beatOffset - beatCursor) * currentSpq;\n beatCursor = change.beatOffset;\n }\n\n // Record the tempo change\n tempoMap.push({\n measureIndex: m,\n beatInMeasure: change.beatOffset,\n timeSeconds: absoluteTime,\n bpm: change.qpm,\n });\n\n if (m === 0 && change.beatOffset === 0) {\n hasInitialTempo = true;\n }\n\n currentQpm = change.qpm;\n currentSpq = 60 / currentQpm;\n }\n\n // Advance time for the remaining beats in the measure\n if (mBeats > beatCursor) {\n absoluteTime += (mBeats - beatCursor) * currentSpq;\n }\n\n // Insert fermata/caesura hold time for this measure (at the measure-start\n // tempo, matching the timeline's within-measure event shifting).\n const measureHolds = holds?.[m];\n if (measureHolds) {\n for (const hold of measureHolds) {\n absoluteTime += hold.extraBeats * measureStartSpq;\n }\n }\n }\n\n // Ensure there's always an initial tempo entry at time 0\n if (!hasInitialTempo) {\n tempoMap.unshift({\n measureIndex: 0,\n beatInMeasure: 0,\n timeSeconds: 0,\n bpm: DEFAULT_BPM,\n });\n }\n\n return { tempoMap, measureStartTimes };\n}\n\n/**\n * Result of {@link buildTempoModel}: a continuous {@link TempoModel} plus the\n * per-measure coordinate arrays the timeline needs to convert measure-relative\n * beats to the model's GLOBAL beat axis.\n */\nexport interface TempoBuild {\n /** Continuous, sub-bar-resolved beat\u2194time map (the single source of timing). */\n model: TempoModel;\n /** Absolute start time (seconds) of each expanded measure. */\n measureStartTimes: number[];\n /** Global quarter-note beat at each expanded measure's start (hold-free axis;\n * holds are time insertions, not beats). Add a measure-relative beat offset\n * to get the global beat for `model.timeAtBeat`. */\n measureStartBeats: number[];\n /** End of the final measure on the same beat axis, including inherited meters. */\n totalBeats: number;\n}\n\n/**\n * Build the continuous {@link TempoModel} for an expanded measure list.\n *\n * Tempo regions come from each measure's `tempos[]`, placed at their real\n * sub-bar beat (so a mid-bar tempo change takes effect mid-bar \u2014 the per-bar\n * model could not express this). A tempo with no later change holds constant\n * to the next region. Fermata/caesura holds become point time-insertions at\n * their (global) beat, so every later event \u2014 at any nesting depth \u2014 shifts\n * uniformly through the gap via `model.timeAtBeat`.\n *\n * `measureStartBeats[i]` is the cumulative global beat at measure `i`'s start;\n * `measureStartTimes[i] = model.timeAtBeat(measureStartBeats[i])`.\n */\nexport function buildTempoModel(globalMeasures: readonly GlobalMeasure[], holds?: HoldSchedule): TempoBuild {\n const regions: TempoRegion[] = [];\n const measureStartBeats: number[] = [];\n\n let cumBeats = 0;\n let currentTime: TimeSignature = { count: DEFAULT_TIME.count, unit: DEFAULT_TIME.unit };\n\n // Collected gradual-tempo (rit./accel.) ramps, resolved to global beats after\n // the full measureStartBeats axis is known (the ramp end may cross bar lines).\n const gradualDecls: { declIdx: number; gt: NonNullable<GlobalMeasure[\"gradualTempo\"]>; startBeat: number }[] = [];\n\n // Pass 1: measure start beats + constant tempo regions from each `tempos[]`.\n // Gradual tempos are collected here and resolved into ramp regions below.\n for (let m = 0; m < globalMeasures.length; m++) {\n const gm = globalMeasures[m]!;\n const nominalTime = playbackNominalTime(gm) ?? gm.time;\n if (nominalTime) currentTime = { count: nominalTime.count, unit: nominalTime.unit };\n const mBeats = measureBeatsFromTime(gm.time ?? currentTime);\n measureStartBeats.push(cumBeats);\n\n if (gm.tempos && gm.tempos.length > 0) {\n const sorted = [...gm.tempos].sort((a, b) => tempoBeat(a, mBeats) - tempoBeat(b, mBeats));\n for (const tempo of sorted) {\n const startBeat = cumBeats + tempoBeat(tempo, mBeats);\n const qpm = effectiveQpm(tempo.bpm, tempo.value.base, tempo.value.dots);\n regions.push({ startBeat, endBeat: Infinity, startBpm: qpm, endBpm: qpm });\n }\n }\n if (gm.gradualTempo) {\n gradualDecls.push({\n declIdx: m,\n gt: gm.gradualTempo,\n startBeat: cumBeats + posBeats(gm.gradualTempo.position.fraction),\n });\n }\n cumBeats += mBeats;\n }\n\n // Default region from beat 0 if the score opens without a tempo.\n if (regions.length === 0 || regions.every((r) => r.startBeat > 1e-9)) {\n regions.unshift({ startBeat: 0, endBeat: Infinity, startBpm: DEFAULT_BPM, endBpm: DEFAULT_BPM });\n }\n\n // Resolve gradual-tempo ramps. `startBpm` defaults to the tempo active at the\n // ramp's start (read from a constant-only model so the ramp begins\n // continuously). A linear-in-BPM ramp region replaces the constant tempo over\n // [start, end); a trailing constant anchor at `end` holds `endBpm` afterwards\n // unless a real tempo (\"a tempo\") already starts there.\n if (gradualDecls.length > 0) {\n const idToIdx = buildMeasureIdToIndex(globalMeasures);\n const constModel = TempoModel.build(regions);\n for (const { declIdx, gt, startBeat } of gradualDecls) {\n const endBeat = resolveGradualEndBeat(gt.end, declIdx, globalMeasures, measureStartBeats, idToIdx);\n if (endBeat === undefined || endBeat <= startBeat + 1e-9) continue;\n const startBpm = gt.startBpm ?? constModel.bpmAtBeat(startBeat);\n regions.push({ startBeat, endBeat, startBpm, endBpm: gt.endBpm });\n const hasAnchorAtEnd = regions.some((r) => Math.abs(r.startBeat - endBeat) < 1e-9);\n if (!hasAnchorAtEnd) {\n regions.push({ startBeat: endBeat, endBeat: Infinity, startBpm: gt.endBpm, endBpm: gt.endBpm });\n }\n }\n }\n\n // Close each region at the next region's start (constant regions are\n // contiguous; ramp regions land exactly on the boundary we inserted at their\n // end; the final region stays open at Infinity).\n regions.sort((a, b) => a.startBeat - b.startBeat);\n for (let i = 0; i < regions.length - 1; i++) {\n regions[i]!.endBeat = regions[i + 1]!.startBeat;\n }\n\n // Insertions (fermata/caesura gaps). Seconds use the tempo at the measure\n // start (matching the legacy hold seconds), computed from a regions-only model.\n const regionsModel = TempoModel.build(regions);\n const insertions: TempoInsertion[] = [];\n if (holds) {\n for (let m = 0; m < holds.length; m++) {\n const measureHolds = holds[m];\n if (!measureHolds || measureHolds.length === 0) continue;\n const startBeat = measureStartBeats[m]!;\n const spb = regionsModel.spbAtBeat(startBeat);\n for (const hold of measureHolds) {\n insertions.push({ beat: startBeat + hold.atBeat, seconds: hold.extraBeats * spb });\n }\n }\n }\n\n const model = insertions.length > 0 ? TempoModel.build(regions, insertions) : regionsModel;\n const measureStartTimes = measureStartBeats.map((b) => model.timeAtBeat(b));\n return { model, measureStartTimes, measureStartBeats, totalBeats: cumBeats };\n}\n\n/** A rhythmic-position fraction (of a whole note) \u2192 quarter-note beats. */\nfunction posBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n/** Map every global-measure id \u2192 its index (for gradual-tempo end resolution). */\nfunction buildMeasureIdToIndex(globalMeasures: readonly GlobalMeasure[]): Map<string, number> {\n const m = new Map<string, number>();\n globalMeasures.forEach((gm, i) => {\n if (gm.id && !m.has(gm.id)) m.set(gm.id, i);\n });\n return m;\n}\n\n/** Resolve a gradual-tempo end (measure id + position) to a global beat. */\nfunction resolveGradualEndBeat(\n end: NonNullable<GlobalMeasure[\"gradualTempo\"]>[\"end\"],\n fromIdx: number,\n globalMeasures: readonly GlobalMeasure[],\n measureStartBeats: readonly number[],\n idToIdx: ReadonlyMap<string, number>,\n): number | undefined {\n let idx = idToIdx.get(end.measure);\n if (idx === undefined) {\n const parsed = Number.parseInt(end.measure, 10);\n if (!Number.isNaN(parsed) && parsed >= 0 && parsed < globalMeasures.length) idx = parsed;\n }\n if (idx === undefined) idx = fromIdx; // same-measure ramp fallback\n const start = measureStartBeats[idx];\n if (start === undefined) return undefined;\n return start + posBeats(end.position.fraction);\n}\n\n/** Global-measure tempo's beat offset within its measure (0 if unlocated). */\nfunction tempoBeat(tempo: NonNullable<GlobalMeasure[\"tempos\"]>[number], mBeats: number): number {\n return tempo.location ? (tempo.location.fraction[0] / tempo.location.fraction[1]) * mBeats : 0;\n}\n\n/**\n * Look up the seconds-per-quarter-note at a given absolute time.\n */\nexport function spqAtTime(tempoMap: readonly TempoMapEntry[], time: number): number {\n let activeQpm = DEFAULT_BPM;\n for (const entry of tempoMap) {\n if (entry.timeSeconds <= time + 1e-9) {\n activeQpm = entry.bpm;\n } else {\n break;\n }\n }\n return 60 / activeQpm;\n}\n", "/**\n * Tempo model \u2014 a continuous, sub-bar-resolved map between musical position\n * (global quarter-note beats) and wall-clock time (seconds).\n *\n * Why this exists: the original tempo handling resolved tempo only at measure\n * granularity (one BPM per bar, applied linearly), so a tempo change mid-bar,\n * a rit./accel., or a fermata-as-tempo-dip could not be expressed. This model\n * integrates a piecewise tempo curve at arbitrary beat resolution, which is\n * exactly what MIDI tempo meta-events express and what a studio click track\n * needs.\n *\n * The model is two layers:\n *\n * 1. **Tempo regions** \u2014 a sorted, contiguous list of `[startBeat, endBeat)`\n * spans, each with a start and end tempo (BPM of a quarter note). Within a\n * region the tempo varies LINEARLY IN BPM over beats: a constant region has\n * `startBpm === endBpm`; a rit./accel. is a ramp. Linear-in-BPM is the\n * conventional reading of a gradual tempo change and integrates to a clean,\n * invertible closed form (logarithmic time, exponential inverse).\n *\n * 2. **Point insertions** \u2014 instantaneous time gaps at a beat that DO NOT\n * advance musical position (a caesura / grand pause is pure silence). These\n * shift all later time without consuming beats.\n *\n * A fermata is modelled as a flat tempo DIP region (a constant region at a\n * slower tempo over the held span), NOT an insertion \u2014 so concurrent notes\n * underneath stretch correctly and following notes shift with zero \"bleed\".\n * (The fermata wiring lives in the hold layer; this module just integrates\n * whatever regions it is given.)\n */\n\n/** Seconds-per-beat \u2194 BPM helpers (quarter-note BPM). */\nconst bpmToSpb = (bpm: number): number => 60 / bpm;\n\n/** Slopes below this (BPM per beat) are treated as constant to avoid blowups. */\nconst RAMP_EPS = 1e-6;\n\n/** A contiguous tempo region in global quarter-note beats. */\nexport interface TempoRegion {\n /** Global quarter-note beat where this region starts (inclusive). */\n startBeat: number;\n /** Global quarter-note beat where this region ends (exclusive). The final\n * region uses `Infinity`. */\n endBeat: number;\n /** Quarter-note BPM at `startBeat`. */\n startBpm: number;\n /** Quarter-note BPM at `endBeat`. Equal to `startBpm` for a constant region;\n * different for a rit. (slower \u2192 smaller BPM) or accel. (faster). */\n endBpm: number;\n}\n\n/** An instantaneous time gap at a beat that does not advance musical position. */\nexport interface TempoInsertion {\n /** Global quarter-note beat at which the gap is inserted. */\n beat: number;\n /** Seconds of silence inserted. */\n seconds: number;\n}\n\n/** A precomputed knot: cumulative time at a region boundary (post-insertions). */\ninterface Knot {\n beat: number;\n /** Cumulative time at `beat`, just AFTER any insertion at this beat. */\n time: number;\n /** BPM at `beat` (region start tempo). */\n bpm: number;\n /** BPM slope (per beat) within the region starting here; 0 if constant. */\n slope: number;\n /** Seconds inserted exactly at this beat (point hold), already folded into\n * `time`; needed so the inverse maps the gap interval back to this beat. */\n insertBefore: number;\n}\n\n/**\n * Integrate the time advanced over `[b0, b]` within a single linear-BPM region.\n * Linear in BPM: `bpm(x) = bpm0 + slope\u00B7(x \u2212 b0)`. Time is `\u222B 60/bpm dx`:\n * - constant (slope\u22480): `60\u00B7(b\u2212b0)/bpm0`\n * - ramp: `(60/slope)\u00B7ln(bpm(b)/bpm0)`\n */\nfunction integrateTime(b0: number, b: number, bpm0: number, slope: number): number {\n const db = b - b0;\n if (Math.abs(slope) < RAMP_EPS) return bpmToSpb(bpm0) * db;\n const bpmB = bpm0 + slope * db;\n return (60 / slope) * Math.log(bpmB / bpm0);\n}\n\n/**\n * Invert {@link integrateTime}: given elapsed seconds `dt` from `b0`, return the\n * beat reached. The inverse of the closed forms above.\n */\nfunction inverseBeat(dt: number, bpm0: number, slope: number): number {\n if (Math.abs(slope) < RAMP_EPS) return (dt * bpm0) / 60;\n // dt = (60/slope)\u00B7ln(bpm/bpm0) \u2192 bpm = bpm0\u00B7exp(dt\u00B7slope/60); db = (bpm\u2212bpm0)/slope\n const bpmAt = bpm0 * Math.exp((dt * slope) / 60);\n return (bpmAt - bpm0) / slope;\n}\n\n/**\n * A continuous tempo map. Construct via {@link TempoModel.build}.\n *\n * All public queries are O(log n) over the region/knot list.\n */\nexport class TempoModel {\n private readonly knots: readonly Knot[];\n\n private constructor(knots: readonly Knot[]) {\n this.knots = knots;\n }\n\n /**\n * Build a model from tempo regions and point insertions.\n *\n * `regions` need not be pre-sorted or gapless; they are sorted by `startBeat`\n * and gaps are filled by extending the previous tempo (a held constant). The\n * first region's start tempo applies from beat 0. `insertions` are point gaps\n * (caesuras) keyed by beat.\n */\n static build(regions: readonly TempoRegion[], insertions: readonly TempoInsertion[] = []): TempoModel {\n const sorted = [...regions].sort((a, b) => a.startBeat - b.startBeat);\n const insByBeat = new Map<number, number>();\n for (const ins of insertions) {\n if (ins.seconds > 0) insByBeat.set(ins.beat, (insByBeat.get(ins.beat) ?? 0) + ins.seconds);\n }\n\n // Collect boundary beats: every region start + every insertion beat.\n const boundarySet = new Set<number>([0]);\n for (const r of sorted) boundarySet.add(r.startBeat);\n for (const ins of insByBeat.keys()) boundarySet.add(ins);\n const boundaries = [...boundarySet].sort((a, b) => a - b);\n\n // Resolve the tempo (bpm + slope) active at a given beat from the regions.\n const tempoAt = (beat: number): { bpm: number; slope: number } => {\n let active: TempoRegion | undefined;\n for (const r of sorted) {\n if (r.startBeat <= beat + 1e-9) active = r;\n else break;\n }\n if (!active) {\n // Before the first region: use the first region's start tempo, or 120.\n const first = sorted[0];\n return { bpm: first ? first.startBpm : 120, slope: 0 };\n }\n const span = active.endBeat - active.startBeat;\n const slope = span > 0 && Number.isFinite(span) ? (active.endBpm - active.startBpm) / span : 0;\n const bpmAtBeat = active.startBpm + slope * (beat - active.startBeat);\n return { bpm: bpmAtBeat, slope };\n };\n\n const knots: Knot[] = [];\n let time = 0;\n let prevBeat = boundaries[0]!;\n let prev = tempoAt(prevBeat);\n // First knot (with any insertion at beat 0).\n const ins0 = insByBeat.get(prevBeat) ?? 0;\n time += ins0;\n knots.push({ beat: prevBeat, time, bpm: prev.bpm, slope: prev.slope, insertBefore: ins0 });\n\n for (let i = 1; i < boundaries.length; i++) {\n const beat = boundaries[i]!;\n // Advance time across [prevBeat, beat) using the tempo active at prevBeat.\n time += integrateTime(prevBeat, beat, prev.bpm, prev.slope);\n const ins = insByBeat.get(beat) ?? 0;\n time += ins;\n const t = tempoAt(beat);\n knots.push({ beat, time, bpm: t.bpm, slope: t.slope, insertBefore: ins });\n prevBeat = beat;\n prev = t;\n }\n\n return new TempoModel(knots);\n }\n\n /** Index of the last knot whose beat \u2264 `beat`. */\n private knotIndexForBeat(beat: number): number {\n const k = this.knots;\n let lo = 0;\n let hi = k.length - 1;\n let ans = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (k[mid]!.beat <= beat + 1e-9) {\n ans = mid;\n lo = mid + 1;\n } else hi = mid - 1;\n }\n return ans;\n }\n\n /** Index of the last knot whose time \u2264 `time`. */\n private knotIndexForTime(time: number): number {\n const k = this.knots;\n let lo = 0;\n let hi = k.length - 1;\n let ans = 0;\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (k[mid]!.time <= time + 1e-9) {\n ans = mid;\n lo = mid + 1;\n } else hi = mid - 1;\n }\n return ans;\n }\n\n /**\n * Absolute time (seconds) at a global quarter-note beat.\n *\n * `endpoint` selects how a point insertion EXACTLY at `beat` is treated:\n * - `\"onset\"` (default): time AFTER the insertion \u2014 what an event STARTING at\n * `beat` wants (it begins once the gap is over).\n * - `\"release\"`: time BEFORE the insertion \u2014 what a note ENDING at `beat`\n * wants (a caesura/gap at the release point isn't part of the note).\n */\n timeAtBeat(beat: number, endpoint: \"onset\" | \"release\" = \"onset\"): number {\n const i = this.knotIndexForBeat(beat);\n const knot = this.knots[i]!;\n const base = knot.time + integrateTime(knot.beat, beat, knot.bpm, knot.slope);\n if (endpoint === \"release\" && Math.abs(knot.beat - beat) < 1e-9) {\n // `knot.time` already folds in `insertBefore`; back it out for a release.\n return base - knot.insertBefore;\n }\n return base;\n }\n\n /**\n * Seconds elapsed over `[beat, beat + beats)` \u2014 a note/event DURATION. The end\n * uses release semantics so a point insertion (caesura) exactly at the release\n * beat is not absorbed into the note's sounding length.\n */\n secondsForBeats(beat: number, beats: number): number {\n return this.timeAtBeat(beat + beats, \"release\") - this.timeAtBeat(beat);\n }\n\n /**\n * Global quarter-note beat at an absolute time (inverse of {@link timeAtBeat}).\n * Within a point-insertion gap, returns the insertion's beat (position is\n * frozen during a caesura).\n */\n beatAtTime(time: number): number {\n const i = this.knotIndexForTime(time);\n const knot = this.knots[i]!;\n const next = this.knots[i + 1];\n if (next) {\n // The music for this knot runs until just before the NEXT knot's point\n // insertion; the interval [musicEnd, next.time) is that insertion's gap,\n // during which position is frozen at next.beat (a caesura holds time).\n const musicEnd = next.time - next.insertBefore;\n if (time >= musicEnd - 1e-9) return next.beat;\n }\n return knot.beat + inverseBeat(time - knot.time, knot.bpm, knot.slope);\n }\n\n /** Seconds-per-quarter-note beat at a global beat. */\n spbAtBeat(beat: number): number {\n const i = this.knotIndexForBeat(beat);\n const knot = this.knots[i]!;\n const bpmHere = knot.bpm + knot.slope * (beat - knot.beat);\n return bpmToSpb(bpmHere);\n }\n\n /** Quarter-note BPM at a global beat. */\n bpmAtBeat(beat: number): number {\n return 60 / this.spbAtBeat(beat);\n }\n\n /**\n * Generate click times (seconds) for a metronome / studio click track over\n * `[startBeat, endBeat)`. `clickBeats` are the beat positions to click,\n * expressed as offsets within each \"grid step\" \u2014 but for the common case\n * callers pass a flat list of global beat positions and we just map them.\n * Returns the absolute time of every beat in `beats`.\n */\n clickTimes(beats: readonly number[]): number[] {\n return beats.map((b) => this.timeAtBeat(b));\n }\n}\n", "/**\n * Dynamics envelope \u2014 couples velocity and CC11 (expression) so a single\n * notated dynamic level drives BOTH axes from one source of truth.\n *\n * Why two axes at all:\n * - Velocity selects the SF2 sample layer (timbre) and the attack level. It is\n * fixed at noteOn and cannot change while a note sustains.\n * - CC11 (expression) is a per-voice amplitude scalar that DOES respond during\n * a held note, so it carries the level and enables long-note shaping\n * (hairpins, messa di voce).\n *\n * Perceived loudness is roughly `velGain(velocity) \u00D7 CC11/127` \u2014 a PRODUCT. If\n * the dynamic level were encoded independently into both axes they would\n * \"stack\" (f\u00B2 too loud, pp\u00B2 inaudible). The fix here is a single mapping\n * `dynamic \u2192 {velocity, cc11}` (`DYNAMIC_AXES`): both axes derive from the same\n * level, so the combined curve is designed, not accidental. Each axis spans a\n * compressed range (velocity ~19 dB, cc11 ~11 dB) so the PRODUCT lands on a\n * musical ~30 dB orchestral range.\n *\n * The envelope models three things over a part's timeline:\n * - ANCHORS: explicit graded dynamics (p, f, \u2026) \u2014 a step function.\n * - RAMPS: hairpins (cresc./dim.) \u2014 both axes interpolate CONTINUOUSLY across\n * the span (velocity per note = anti-zipper; CC11 on a fine grid = smooth\n * swell), so a multi-measure cresc over mixed short/long notes shapes\n * correctly and a held note gets a real messa di voce. Consecutive hairpins\n * chain (a dim. starting where a cresc. ends inherits its peak level), and an\n * OPEN hairpin (no written target) moves one dynamic step in its direction.\n * - ATTACKS: accent groups alter the note-on velocity at their onset without\n * swelling unrelated held notes. An accent's `residualValue` additionally\n * drops the persistent level, which is how `fp` is encoded.\n *\n * CC11 is owned exclusively by this system (the mute keyswitch is timbre-only \u2014\n * CC74/71 \u2014 and no longer touches CC11). CC7 (channel volume) belongs to the\n * mixer and is never written here.\n */\n\nimport {\n walkSequenceEvents,\n type DynamicGroup,\n type DynamicValue,\n type GlobalMeasure,\n type GradualDynamicGroup,\n type Part,\n} from \"@viritura/core\";\nimport type { ImpliedSectionDynamicAnchor } from \"./sectionDynamics\";\nimport type { MidiEvent } from \"./types\";\nimport type { TempoModel } from \"./tempoModel\";\nimport { resolvePartStaffMeterTable, staffMeterRatioAt, type StaffMeterTable } from \"./staffMeterTiming\";\n\n/** CC 11 (expression). The coupled dynamics system is its sole owner. */\nconst CC_EXPRESSION = 11;\n\n/** Time-coincidence tolerance (seconds). */\nconst EPS = 1e-6;\n\n/** The two coupled MIDI axes a dynamic level maps to. */\nexport interface DynamicAxes {\n /** noteOn velocity (1\u2013127): selects sample layer + attack level. */\n velocity: number;\n /** CC11 expression (0\u2013127): sustained level, shapeable mid-note. */\n cc11: number;\n}\n\n/** Neutral default (mf) when no dynamic marking precedes a note. */\nexport const DEFAULT_DYNAMIC: DynamicAxes = { velocity: 84, cc11: 100 };\n\n/**\n * Coupled velocity/CC11 calibration per dynamic marking.\n *\n * Velocity spans 30\u2192127, CC11 spans 18\u2192127; the product targets ~30 dB. These\n * are tuning constants \u2014 adjust by ear, keeping both axes monotonic with level.\n *\n * The score stores semantic values; these are the generic fallback realization\n * for an SF2/MIDI backend. Instrument-specific response profiles can project\n * the same semantic scale differently.\n */\nexport const DYNAMIC_AXES: Record<DynamicValue, DynamicAxes> = {\n n: { velocity: 1, cc11: 0 },\n pppppp: { velocity: 22, cc11: 11 },\n ppppp: { velocity: 28, cc11: 17 },\n pppp: { velocity: 34, cc11: 24 },\n ppp: { velocity: 42, cc11: 34 },\n pp: { velocity: 52, cc11: 50 },\n p: { velocity: 64, cc11: 66 },\n mp: { velocity: 74, cc11: 82 },\n mf: { velocity: 84, cc11: 100 },\n f: { velocity: 98, cc11: 112 },\n ff: { velocity: 112, cc11: 122 },\n fff: { velocity: 122, cc11: 127 },\n // CC11 saturates at fff, so the extreme fortissimos separate on velocity\n // alone \u2014 which is also what selects the loudest SF2 sample layers.\n ffff: { velocity: 124, cc11: 127 },\n fffff: { velocity: 126, cc11: 127 },\n ffffff: { velocity: 127, cc11: 127 },\n};\n\n/** Look up the coupled axes for a dynamic marking, or undefined if unknown. */\nexport function dynamicToAxes(value: DynamicValue): DynamicAxes {\n return DYNAMIC_AXES[value];\n}\n\n/** Axes for a standard dynamic value. */\nfunction axesOf(value: DynamicValue): DynamicAxes {\n return DYNAMIC_AXES[value];\n}\n\n/** Graded dynamic ladder (soft \u2192 loud) for hairpin fallback stepping. */\nconst DYNAMIC_LADDER: readonly DynamicValue[] = [\n \"n\",\n \"pppppp\",\n \"ppppp\",\n \"pppp\",\n \"ppp\",\n \"pp\",\n \"p\",\n \"mp\",\n \"mf\",\n \"f\",\n \"ff\",\n \"fff\",\n \"ffff\",\n \"fffff\",\n \"ffffff\",\n];\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Envelope model\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** An explicit graded dynamic at an absolute time (step). */\ninterface DynamicAnchor {\n time: number;\n velocity: number;\n cc11: number;\n}\n\n/** A hairpin span: both axes interpolate from `start` to `end` over the span. */\ninterface DynamicRamp {\n groupId: string;\n startTime: number;\n endTime: number;\n start: DynamicAxes;\n end: DynamicAxes;\n}\n\n/** A dynamic attack override at one onset. */\ninterface DynamicAttack {\n time: number;\n attackVelocity: number;\n}\n\n/** A part's coupled dynamics over time. */\nexport interface DynamicsEnvelope {\n anchors: readonly DynamicAnchor[];\n ramps: readonly DynamicRamp[];\n attacks: readonly DynamicAttack[];\n}\n\n/** Convert a measure-relative fraction (of a whole note) to quarter-note beats. */\nfunction fractionToBeats(frac: readonly [number, number]): number {\n if (!frac || frac[1] === 0) return 0;\n return (frac[0] / frac[1]) * 4;\n}\n\n/** Clamp to [0,1]. */\nfunction clamp01(x: number): number {\n return x < 0 ? 0 : x > 1 ? 1 : x;\n}\n\n/** Linear blend of two axes at fraction f (rounded to ints). */\nfunction lerpAxes(a: DynamicAxes, b: DynamicAxes, frac: number): DynamicAxes {\n const f = clamp01(frac);\n return {\n velocity: Math.round(a.velocity + (b.velocity - a.velocity) * f),\n cc11: Math.round(a.cc11 + (b.cc11 - a.cc11) * f),\n };\n}\n\n/** Nearest ladder index to a given level (by velocity). */\nfunction nearestLadderIndex(axes: DynamicAxes): number {\n let best = 0;\n let bestDist = Infinity;\n for (let i = 0; i < DYNAMIC_LADDER.length; i++) {\n const dist = Math.abs(DYNAMIC_AXES[DYNAMIC_LADDER[i]!]!.velocity - axes.velocity);\n if (dist < bestDist) {\n bestDist = dist;\n best = i;\n }\n }\n return best;\n}\n\n/** Step one rung along the ladder in `dir` (+1 louder, \u22121 softer). */\nfunction stepRung(axes: DynamicAxes, dir: 1 | -1): DynamicAxes {\n const i = nearestLadderIndex(axes);\n const j = Math.max(0, Math.min(DYNAMIC_LADDER.length - 1, i + dir));\n return DYNAMIC_AXES[DYNAMIC_LADDER[j]!]!;\n}\n\n/** Sample only the anchor step function (no ramps) at time `t`. */\nfunction sampleAnchorsAxes(anchors: readonly DynamicAnchor[], t: number): DynamicAxes {\n let active: DynamicAxes = DEFAULT_DYNAMIC;\n for (const a of anchors) {\n if (a.time > t + EPS) break;\n active = { velocity: a.velocity, cc11: a.cc11 };\n }\n return active;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Build\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** Map every global measure id \u2192 its original index (for hairpin end lookup). */\nfunction buildMeasureIdToOrig(globalMeasures: readonly GlobalMeasure[]): Map<string, number> {\n const m = new Map<string, number>();\n globalMeasures.forEach((gm, i) => {\n if (gm.id) m.set(gm.id, i);\n });\n return m;\n}\n\ninterface TimedDynamicGroup {\n group: DynamicGroup;\n time: number;\n expandedIdx: number;\n}\n\ninterface TimedGradualText {\n id: string;\n startTime: number;\n dir: 1 | -1;\n expandedIdx: number;\n}\n\nfunction gradualTextDirection(text: string): 1 | -1 | undefined {\n const normalized = text.trim().toLocaleLowerCase();\n if (/^cresc(?:endo)?\\.?$/.test(normalized)) return 1;\n if (/^(?:dim(?:inuendo)?|decresc(?:endo)?)\\.?$/.test(normalized)) return -1;\n return undefined;\n}\n\nfunction collectMeasureDynamicGroups(\n pm: Part[\"measures\"][number],\n originalMeasureIndex: number,\n measureStartBeat: number,\n model: TempoModel,\n expandedIdx: number,\n groups: TimedDynamicGroup[],\n staffMeterTable: StaffMeterTable,\n): void {\n for (const group of pm.dynamics ?? []) {\n const ratio = staffMeterRatioAt(staffMeterTable, originalMeasureIndex, group.staff);\n groups.push({\n group,\n time: model.timeAtBeat(measureStartBeat + fractionToBeats(group.position.fraction) * ratio),\n expandedIdx,\n });\n }\n}\n\nfunction collectMeasureGradualTexts(\n pm: Part[\"measures\"][number],\n originalMeasureIndex: number,\n measureStartBeat: number,\n model: TempoModel,\n expandedIdx: number,\n gradualTexts: TimedGradualText[],\n staffMeterTable: StaffMeterTable,\n): void {\n for (let index = 0; index < (pm.expressions?.length ?? 0); index++) {\n const expression = pm.expressions![index]!;\n const dir = gradualTextDirection(expression.text);\n if (!dir) continue;\n gradualTexts.push({\n id: `text-gradual-${expandedIdx}-${index}`,\n startTime: model.timeAtBeat(\n measureStartBeat +\n fractionToBeats(expression.position.fraction) *\n staffMeterRatioAt(staffMeterTable, originalMeasureIndex, expression.staff),\n ),\n dir,\n expandedIdx,\n });\n }\n}\n\n/** Resolve a hairpin's end-measure start beat (nearest forward occurrence). */\nfunction resolveEndMeasureLocation(\n endMeasureId: string,\n fromExpandedIdx: number,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n idToOrig: ReadonlyMap<string, number>,\n): { measureStartBeat: number; originalMeasureIndex: number } | undefined {\n let targetOrig = idToOrig.get(endMeasureId);\n if (targetOrig === undefined) {\n // Fallback: some scores use the numeric measure index as the id.\n const parsed = Number.parseInt(endMeasureId, 10);\n if (!Number.isNaN(parsed)) targetOrig = parsed;\n }\n if (targetOrig === undefined) return undefined;\n for (let j = fromExpandedIdx; j < measureOrder.length; j++) {\n if (measureOrder[j] === targetOrig) {\n return { measureStartBeat: measureStartBeats[j]!, originalMeasureIndex: targetOrig };\n }\n }\n for (let j = 0; j < measureOrder.length; j++) {\n if (measureOrder[j] === targetOrig) {\n return { measureStartBeat: measureStartBeats[j]!, originalMeasureIndex: targetOrig };\n }\n }\n return undefined;\n}\n\n/** Axes of an explicit (authored) dynamic anchor at (\u2248) `time`, or undefined. */\nfunction explicitAnchorAt(anchors: readonly DynamicAnchor[], time: number): DynamicAxes | undefined {\n for (const a of anchors) {\n if (Math.abs(a.time - time) <= EPS) return { velocity: a.velocity, cc11: a.cc11 };\n }\n return undefined;\n}\n\n/** A hairpin with resolved absolute start/end times and direction. */\ninterface TimedHairpin {\n groupId: string;\n startTime: number;\n endTime: number;\n dir: 1 | -1;\n start?: DynamicAxes;\n startsAfterSilence: boolean;\n measureStartTime: number;\n}\n\nfunction measureHasSoundingNotes(part: Part, measureIndex: number | undefined): boolean {\n if (measureIndex === undefined) return false;\n return (part.measures[measureIndex]?.sequences ?? []).some((sequence) =>\n [...walkSequenceEvents(sequence.content)].some(({ event }) => !!event.notes?.length || !!event.kitNotes?.length),\n );\n}\n\nfunction startsAfterSilentMeasure(part: Part, measureOrder: readonly number[], expandedIdx: number): boolean {\n return expandedIdx > 0 && !measureHasSoundingNotes(part, measureOrder[expandedIdx - 1]);\n}\n\n/**\n * Resolve timed hairpins to ramps in time order, CHAINING consecutive ones:\n * when a hairpin begins exactly where the previous one ended (and no explicit\n * dynamic is written there), it inherits the previous ramp's end level as its\n * start. This makes a messa di voce (a crescendo and decrescendo meeting at a\n * peak) connect smoothly instead of snapping back to the standing level at the\n * seam.\n *\n * Endpoint rules:\n * - START: an explicit dynamic written at the start wins; else the chained\n * previous end (if the hairpins meet); else the standing (sampled) level.\n * - END: an explicit dynamic written at the end wins; else \u2014 for an OPEN\n * hairpin with no written target \u2014 step ONE ladder rung in the hairpin's\n * direction (cresc. \u2192 one louder, dim. \u2192 one softer). So a chained\n * mf-crescendo\u2192f then open decrescendo lands back on mf (f one rung down),\n * giving a symmetric swell.\n */\nfunction resolveRamps(timed: readonly TimedHairpin[], anchors: readonly DynamicAnchor[]): DynamicRamp[] {\n const ordered = [...timed].sort((a, b) => a.startTime - b.startTime);\n const ramps: DynamicRamp[] = [];\n let prev: DynamicRamp | undefined;\n for (const {\n groupId,\n startTime,\n endTime,\n dir,\n start: authoredStart,\n startsAfterSilence,\n measureStartTime,\n } of ordered) {\n if (endTime <= startTime + EPS) continue;\n const chained = prev !== undefined && Math.abs(prev.endTime - startTime) <= EPS;\n const currentMeasureAnchor = anchors.some(\n (anchor) => anchor.time >= measureStartTime - EPS && anchor.time <= startTime + EPS,\n );\n const start =\n authoredStart ??\n explicitAnchorAt(anchors, startTime) ??\n (startsAfterSilence && !currentMeasureAnchor\n ? axesOf(\"p\")\n : chained\n ? prev!.end\n : sampleAnchorsAxes(anchors, startTime));\n const end = explicitAnchorAt(anchors, endTime) ?? stepRung(start, dir);\n const ramp: DynamicRamp = { groupId, startTime, endTime, start, end };\n ramps.push(ramp);\n prev = ramp;\n }\n return ramps;\n}\n\nfunction collectExpandedDynamics(\n part: Part,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n staffMeterTable: StaffMeterTable,\n timedGroups: TimedDynamicGroup[],\n gradualTexts: TimedGradualText[],\n): void {\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const originalMeasureIndex = measureOrder[expandedIdx]!;\n const measure = part.measures[originalMeasureIndex];\n if (!measure) continue;\n const measureStartBeat = measureStartBeats[expandedIdx]!;\n collectMeasureDynamicGroups(\n measure,\n originalMeasureIndex,\n measureStartBeat,\n model,\n expandedIdx,\n timedGroups,\n staffMeterTable,\n );\n collectMeasureGradualTexts(\n measure,\n originalMeasureIndex,\n measureStartBeat,\n model,\n expandedIdx,\n gradualTexts,\n staffMeterTable,\n );\n }\n}\n\n/**\n * Build a part's dynamics envelope from standard dynamic groups. Walks the\n * expanded measure order, so repeated material yields one instance per pass.\n */\nexport function buildDynamicsEnvelope(\n part: Part,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n globalMeasures: readonly GlobalMeasure[],\n impliedAnchors: readonly ImpliedSectionDynamicAnchor[] = [],\n resolvedStaffMeters?: StaffMeterTable,\n): DynamicsEnvelope {\n const anchors: DynamicAnchor[] = [];\n const attacks: DynamicAttack[] = [];\n const timedGroups: TimedDynamicGroup[] = [];\n const gradualTexts: TimedGradualText[] = [];\n const idToOrig = buildMeasureIdToOrig(globalMeasures);\n const staffMeterTable = resolvedStaffMeters ?? resolvePartStaffMeterTable(part, globalMeasures);\n\n // Pass 1: collect and order every dynamic group on the expanded timeline.\n collectExpandedDynamics(part, measureOrder, measureStartBeats, model, staffMeterTable, timedGroups, gradualTexts);\n const typeRank: Record<DynamicGroup[\"type\"], number> = { immediate: 0, relative: 1, accent: 2, gradual: 3 };\n timedGroups.sort(\n (a, b) =>\n a.time - b.time || typeRank[a.group.type] - typeRank[b.group.type] || a.group.id.localeCompare(b.group.id),\n );\n\n // Pass 2: resolve persistent levels and onset attacks.\n for (const { group, time } of timedGroups) {\n if (group.type === \"immediate\") {\n const body = axesOf(group.value);\n anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });\n } else if (group.type === \"relative\") {\n const current = sampleAnchorsAxes(anchors, time - EPS);\n const next = stepRung(current, group.relativeValue === \"louder\" ? 1 : -1);\n anchors.push({ time, velocity: next.velocity, cc11: next.cc11 });\n } else if (group.type === \"accent\") {\n // `value` is the attack; `residualValue` (the \"p\" of \"fp\") is the level\n // that persists from this onset onward.\n attacks.push({ time, attackVelocity: axesOf(group.value).velocity });\n if (group.residualValue !== undefined) {\n const body = axesOf(group.residualValue);\n anchors.push({ time, velocity: body.velocity, cc11: body.cc11 });\n }\n }\n }\n for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {\n const measureIndex = measureOrder[expandedIdx]!;\n for (const implied of impliedAnchors) {\n if (implied.measureIndex !== measureIndex) continue;\n const time = model.timeAtBeat(measureStartBeats[expandedIdx]! + fractionToBeats(implied.position));\n if (anchors.some((anchor) => Math.abs(anchor.time - time) <= EPS)) continue;\n const axes = axesOf(implied.value);\n anchors.push({ time, velocity: axes.velocity, cc11: axes.cc11 });\n }\n }\n anchors.sort((a, b) => a.time - b.time);\n\n // Pass 3: resolve gradual end times, then chain them into ramps.\n const timed: TimedHairpin[] = [];\n for (const { group, time: startTime, expandedIdx } of timedGroups) {\n if (group.type !== \"gradual\") continue;\n const gradual: GradualDynamicGroup = group;\n const endMeasure = resolveEndMeasureLocation(\n gradual.end.measure,\n expandedIdx,\n measureOrder,\n measureStartBeats,\n idToOrig,\n );\n if (endMeasure === undefined) continue;\n const endRatio = staffMeterRatioAt(\n staffMeterTable,\n endMeasure.originalMeasureIndex,\n gradual.staffEnd ?? gradual.staff,\n );\n const endTime = model.timeAtBeat(\n endMeasure.measureStartBeat + fractionToBeats(gradual.end.position.fraction) * endRatio,\n );\n timed.push({\n groupId: gradual.id,\n startTime,\n endTime,\n dir: gradual.wedgeType === \"increasing\" ? 1 : -1,\n start: gradual.value === undefined ? undefined : axesOf(gradual.value),\n startsAfterSilence: startsAfterSilentMeasure(part, measureOrder, expandedIdx),\n measureStartTime: model.timeAtBeat(measureStartBeats[expandedIdx]!),\n });\n }\n for (const text of gradualTexts) {\n if (timed.some((hairpin) => Math.abs(hairpin.startTime - text.startTime) <= EPS)) continue;\n const endpoint = anchors.find((anchor) => anchor.time > text.startTime + EPS);\n if (!endpoint) continue;\n timed.push({\n groupId: text.id,\n startTime: text.startTime,\n endTime: endpoint.time,\n dir: text.dir,\n startsAfterSilence: startsAfterSilentMeasure(part, measureOrder, text.expandedIdx),\n measureStartTime: model.timeAtBeat(measureStartBeats[text.expandedIdx]!),\n });\n }\n const ramps = resolveRamps(timed, anchors);\n attacks.sort((a, b) => a.time - b.time);\n\n return { anchors, ramps, attacks };\n}\n\n/** Whether the part declares any dynamics at all. */\nexport function hasAnyDynamics(env: DynamicsEnvelope): boolean {\n return env.anchors.length > 0 || env.ramps.length > 0 || env.attacks.length > 0;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// Sampling\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n/** The ramp covering time `t`, if any. */\nfunction rampAt(env: DynamicsEnvelope, t: number): DynamicRamp | undefined {\n for (const r of env.ramps) {\n if (t >= r.startTime - EPS && t <= r.endTime + EPS) return r;\n }\n return undefined;\n}\n\n/**\n * Sample the coupled axes (velocity + cc11) at absolute time `t`. A hairpin ramp\n * interpolates continuously across its span; elsewhere the anchor step holds.\n * Attacks do not alter the sampled standing level; see `noteVelocityAt`.\n */\nexport function sampleDynamics(env: DynamicsEnvelope, t: number): DynamicAxes {\n const r = rampAt(env, t);\n if (r) {\n const frac = (t - r.startTime) / (r.endTime - r.startTime);\n return lerpAxes(r.start, r.end, frac);\n }\n return sampleAnchorsAxes(env.anchors, t);\n}\n\n/** An attack override at (approximately) time `t`, if any. */\nfunction attackAt(env: DynamicsEnvelope, t: number): DynamicAttack | undefined {\n for (const c of env.attacks) {\n if (Math.abs(c.time - t) <= EPS) return c;\n }\n return undefined;\n}\n\n/**\n * noteOn velocity at an onset: an authored attack velocity\n * overrides the sampled level; otherwise the sampled (possibly ramped) velocity.\n */\nexport function noteVelocityAt(env: DynamicsEnvelope, t: number): number {\n const c = attackAt(env, t);\n return c ? c.attackVelocity : sampleDynamics(env, t).velocity;\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// CC11 emission\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nexport interface Cc11Event {\n time: number;\n value: number;\n}\n\n/** Exact events where a linear ramp crosses a rounded 7-bit CC11 boundary. */\nfunction rampQuantizationEvents(ramp: DynamicRamp): Cc11Event[] {\n const start = ramp.start.cc11;\n const end = ramp.end.cc11;\n if (start === end || ramp.endTime <= ramp.startTime + EPS) {\n return [\n { time: ramp.startTime, value: start },\n { time: ramp.endTime, value: end },\n ];\n }\n const direction = end > start ? 1 : -1;\n const events: Cc11Event[] = [{ time: ramp.startTime, value: start }];\n for (let value = start + direction; direction > 0 ? value <= end : value >= end; value += direction) {\n const boundary = value - direction * 0.5;\n const fraction = (boundary - start) / (end - start);\n events.push({\n time: ramp.startTime + fraction * (ramp.endTime - ramp.startTime),\n value,\n });\n }\n events.push({ time: ramp.endTime, value: end });\n return events;\n}\n\n/**\n * Resolve the ordered CC11 events for a part's envelope: a baseline at part\n * start, anchors, and exact rounded-value crossings. Consecutive equal values\n * are deduped, so a monotonic ramp emits no more than 127 useful writes.\n */\nexport function cc11Events(env: DynamicsEnvelope): Cc11Event[] {\n const raw: Cc11Event[] = [{ time: 0, value: sampleDynamics(env, 0).cc11 }];\n for (const anchor of env.anchors) raw.push({ time: anchor.time, value: anchor.cc11 });\n for (const ramp of env.ramps) raw.push(...rampQuantizationEvents(ramp));\n raw.sort((a, b) => a.time - b.time);\n\n const out: Cc11Event[] = [];\n for (const e of raw) {\n const prev = out[out.length - 1];\n if (prev && Math.abs(prev.time - e.time) <= EPS) {\n out[out.length - 1] = e; // same instant: later write wins\n continue;\n }\n if (prev && prev.value === e.value) continue; // unchanged level\n out.push(e);\n }\n if (out.length === 0 || out[0]!.time > EPS) {\n out.unshift({ time: 0, value: DEFAULT_DYNAMIC.cc11 });\n }\n return out;\n}\n\n/**\n * Append a part's coupled-dynamics CC11 (expression) MIDI events: a baseline at\n * part start plus one event per level change. CC11 is the sole dynamics level\n * axis (the mute keyswitch is timbre-only). The global sort places controlChange\n * before any noteOn at the same time.\n */\nexport function emitDynamicsCc11(\n env: DynamicsEnvelope,\n partIndex: number,\n channel: number,\n out: MidiEvent[],\n playbackLaneId?: string,\n): void {\n for (const ev of cc11Events(env)) {\n out.push({\n type: \"controlChange\",\n time: ev.time,\n midiNote: 0,\n velocity: 0,\n partIndex,\n playbackLaneId,\n channel,\n cc: CC_EXPRESSION,\n value: ev.value,\n });\n }\n}\n", "export { compileDynamicProgram, laneForScope, laneForSequence, playbackLaneId } from \"./dynamicProgram\";\nexport { realizeDynamicsEnvelope, selectDynamicResponseProfile } from \"./dynamicRealization\";\nexport type {\n DynamicPlaybackDiagnostic,\n DynamicProgram,\n DynamicResponseProfile,\n PlaybackLane,\n PlaybackLaneId,\n} from \"./types\";\n", "import type { DynamicGroup, GlobalMeasure, Part, Sequence, TextExpression } from \"@viritura/core\";\nimport { buildDynamicsEnvelope } from \"../dynamicsEnvelope\";\nimport type { TempoModel } from \"../tempoModel\";\nimport { realizeDynamicsEnvelope, selectDynamicResponseProfile } from \"./dynamicRealization\";\nimport type { DynamicPlaybackDiagnostic, DynamicProgram, PlaybackLane, PlaybackLaneId } from \"./types\";\nimport type { ImpliedSectionDynamicAnchor } from \"../sectionDynamics\";\nimport type { StaffMeterTable } from \"../staffMeterTiming\";\n\ninterface LaneScope {\n staff: number;\n voice: string;\n}\n\nfunction sequenceVoice(sequence: Sequence, index: number): string {\n return sequence.voice ?? `sequence:${index}`;\n}\n\nfunction collectContentStaves(content: Readonly<Sequence[\"content\"]>, staves: Set<number>): void {\n for (const item of content) {\n if (item.type === \"event\") {\n if (item.staff !== undefined) staves.add(item.staff);\n } else if (item.type === \"tuplet\") {\n collectContentStaves(item.content, staves);\n } else if (item.type === \"grace\" || item.type === \"tremolo\") {\n for (const event of item.content) {\n if (event.staff !== undefined) staves.add(event.staff);\n }\n }\n }\n}\n\nexport function playbackLaneId(partIndex: number, staff?: number, voice?: string): PlaybackLaneId {\n return staff === undefined || voice === undefined\n ? `part:${partIndex}`\n : `part:${partIndex}:staff:${staff}:voice:${voice}`;\n}\n\nfunction collectLaneScopes(part: Part): LaneScope[] {\n const byId = new Map<string, LaneScope>();\n for (const measure of part.measures) {\n measure.sequences.forEach((sequence, index) => {\n const voice = sequenceVoice(sequence, index);\n const staves = new Set([sequence.staff ?? 1]);\n collectContentStaves(sequence.content, staves);\n for (const staff of staves) {\n const scope = { staff, voice };\n byId.set(`${scope.staff}\\u0000${scope.voice}`, scope);\n }\n });\n }\n return byId.size > 0 ? [...byId.values()] : [{ staff: 1, voice: \"sequence:0\" }];\n}\n\nfunction appliesToLane(group: DynamicGroup | TextExpression, scope: LaneScope): boolean {\n return (\n (group.staff === undefined || group.staff === scope.staff) &&\n (group.voice === undefined || group.voice === scope.voice)\n );\n}\n\nfunction withLaneDynamics(part: Part, scope: LaneScope): Part {\n return {\n ...part,\n measures: part.measures.map((measure) => ({\n ...measure,\n dynamics: measure.dynamics?.filter((group) => appliesToLane(group, scope)),\n expressions: measure.expressions?.filter((expression) => appliesToLane(expression, scope)),\n })),\n };\n}\n\nfunction collectScopeDiagnostics(part: Part, scopes: readonly LaneScope[]): DynamicPlaybackDiagnostic[] {\n const diagnostics: DynamicPlaybackDiagnostic[] = [];\n const voices = new Set(scopes.map((scope) => scope.voice));\n const reportedMissing = new Set<string>();\n for (const measure of part.measures) {\n for (const group of measure.dynamics ?? []) {\n if (group.voice && !voices.has(group.voice) && !reportedMissing.has(group.id)) {\n reportedMissing.add(group.id);\n diagnostics.push({\n code: \"missing-voice\",\n message: `Dynamic group ${group.id} targets voice ${group.voice}, which does not exist in the part.`,\n groupIds: [group.id],\n });\n }\n }\n const groups = measure.dynamics ?? [];\n for (let i = 0; i < groups.length; i++) {\n const left = groups[i]!;\n if (left.staff === undefined || left.voice !== undefined) continue;\n for (let j = i + 1; j < groups.length; j++) {\n const right = groups[j]!;\n if (right.staff !== undefined || right.voice === undefined) continue;\n if (\n left.position.fraction[0] * right.position.fraction[1] !==\n right.position.fraction[0] * left.position.fraction[1]\n )\n continue;\n if (!scopes.some((scope) => scope.staff === left.staff && scope.voice === right.voice)) continue;\n diagnostics.push({\n code: \"scope-conflict\",\n message: `Dynamic groups ${left.id} and ${right.id} have intersecting equal-specificity scopes at the same time.`,\n groupIds: [left.id, right.id].sort(),\n });\n }\n }\n }\n return diagnostics;\n}\n\nfunction collectRampDiagnostics(lanes: ReadonlyMap<PlaybackLaneId, PlaybackLane>): DynamicPlaybackDiagnostic[] {\n const diagnostics: DynamicPlaybackDiagnostic[] = [];\n const reported = new Set<string>();\n for (const lane of lanes.values()) {\n const ramps = [...lane.envelope.ramps].sort(\n (left, right) => left.startTime - right.startTime || left.groupId.localeCompare(right.groupId),\n );\n for (let index = 1; index < ramps.length; index++) {\n const previous = ramps[index - 1]!;\n const current = ramps[index]!;\n if (current.startTime >= previous.endTime - 1e-6) continue;\n const groupIds = [previous.groupId, current.groupId].sort();\n const key = groupIds.join(\"\\u0000\");\n if (reported.has(key)) continue;\n reported.add(key);\n diagnostics.push({\n code: \"overlapping-gradual\",\n message: `Dynamic groups ${groupIds.join(\" and \")} overlap in playback lane ${lane.id}.`,\n groupIds,\n });\n }\n }\n return diagnostics;\n}\n\n/** Compile standard MNX groups into independently controllable playback lanes. */\nexport function compileDynamicProgram(\n part: Part,\n partIndex: number,\n measureOrder: readonly number[],\n measureStartBeats: readonly number[],\n model: TempoModel,\n globalMeasures: readonly GlobalMeasure[],\n gmProgram = -1,\n impliedAnchors: readonly ImpliedSectionDynamicAnchor[] = [],\n staffMeterTable?: StaffMeterTable,\n): DynamicProgram {\n const scoped = part.measures.some((measure) =>\n measure.dynamics?.some((group) => group.staff !== undefined || group.voice !== undefined),\n );\n const scopes = collectLaneScopes(part);\n const selectedScopes = scoped ? scopes : [scopes[0]!];\n const profile = selectDynamicResponseProfile(gmProgram);\n const lanes = new Map<PlaybackLaneId, PlaybackLane>();\n\n for (const scope of selectedScopes) {\n const id = scoped ? playbackLaneId(partIndex, scope.staff, scope.voice) : playbackLaneId(partIndex);\n const lanePart = scoped ? withLaneDynamics(part, scope) : part;\n const envelope = realizeDynamicsEnvelope(\n buildDynamicsEnvelope(\n lanePart,\n measureOrder,\n measureStartBeats,\n model,\n globalMeasures,\n impliedAnchors,\n staffMeterTable,\n ),\n profile,\n );\n lanes.set(id, { id, partIndex, staff: scope.staff, voice: scope.voice, envelope });\n }\n\n return {\n lanes,\n diagnostics: [...collectScopeDiagnostics(part, scopes), ...collectRampDiagnostics(lanes)],\n scoped,\n };\n}\n\n/** Resolve a sequence to its compiled lane. */\nexport function laneForSequence(\n program: DynamicProgram,\n partIndex: number,\n sequence: Sequence,\n sequenceIndex: number,\n): PlaybackLane {\n return laneForScope(program, partIndex, sequence.staff ?? 1, sequenceVoice(sequence, sequenceIndex));\n}\n\n/** Resolve an exact sounding staff/voice scope, including event-level\n * cross-staff overrides within a sequence. */\nexport function laneForScope(program: DynamicProgram, partIndex: number, staff: number, voice: string): PlaybackLane {\n if (!program.scoped) return program.lanes.get(playbackLaneId(partIndex))!;\n const id = playbackLaneId(partIndex, staff, voice);\n return program.lanes.get(id) ?? program.lanes.values().next().value!;\n}\n", "import type { DynamicsEnvelope, DynamicAxes } from \"../dynamicsEnvelope\";\nimport type { DynamicResponseProfile } from \"./types\";\n\nconst STRUCK_PROGRAMS = new Set([46]); // Orchestral harp.\n\n/** Select a runtime response class from a zero-based General MIDI program. */\nexport function selectDynamicResponseProfile(program: number): DynamicResponseProfile {\n if ((program >= 0 && program <= 7) || (program >= 24 && program <= 39) || STRUCK_PROGRAMS.has(program)) {\n return \"struck-plucked\";\n }\n if (program >= 16 && program <= 23) return \"organ-fixed-attack\";\n if ((program >= 40 && program <= 45) || (program >= 47 && program <= 79)) return \"sustained-expressive\";\n return \"fallback\";\n}\n\nfunction realizeAxes(axes: DynamicAxes, profile: DynamicResponseProfile): DynamicAxes {\n if (axes.cc11 === 0) return { velocity: 1, cc11: 0 };\n switch (profile) {\n case \"struck-plucked\":\n return { velocity: axes.velocity, cc11: 118 };\n case \"organ-fixed-attack\":\n return { velocity: 84, cc11: axes.cc11 };\n case \"sustained-expressive\":\n case \"fallback\":\n return axes;\n }\n}\n\n/** Project one semantic envelope through an instrument response profile. */\nexport function realizeDynamicsEnvelope(envelope: DynamicsEnvelope, profile: DynamicResponseProfile): DynamicsEnvelope {\n if (profile === \"fallback\" || profile === \"sustained-expressive\") return envelope;\n return {\n anchors: envelope.anchors.map((anchor) => ({ ...anchor, ...realizeAxes(anchor, profile) })),\n ramps: envelope.ramps.map((ramp) => ({\n ...ramp,\n start: realizeAxes(ramp.start, profile),\n end: realizeAxes(ramp.end, profile),\n })),\n attacks: envelope.attacks.map((attack) => ({\n ...attack,\n attackVelocity: profile === \"organ-fixed-attack\" ? 84 : attack.attackVelocity,\n })),\n };\n}\n", "/** Source part IDs, with deterministic positional fallbacks for missing IDs. */\nexport function stablePartIds(rawMnx: unknown): string[] {\n const raw = typeof rawMnx === \"string\" ? (JSON.parse(rawMnx) as unknown) : rawMnx;\n if (!raw || typeof raw !== \"object\" || !(\"parts\" in raw) || !Array.isArray(raw.parts)) return [];\n return raw.parts.map((part: unknown, index: number) => {\n if (part && typeof part === \"object\" && \"id\" in part && typeof part.id === \"string\" && part.id.length > 0) {\n return part.id;\n }\n return `#${index}`;\n });\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["paintFilledBezierSolid", "mnxJson", "mnxJson", "measureBeats", "durationBeats", "measureBeats", "fractionToBeats", "playbackLaneId", "durationBeats", "cursor", "item"]
|
|
7
7
|
}
|