@real-music-packages/web-core 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/promo.js +9 -3
- package/dist/promo.js.map +1 -1
- package/dist/scene/index.js +33 -1
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/promo.js
CHANGED
|
@@ -251,7 +251,7 @@ function extractGeometry(osmd, canvas, inkSumThreshold) {
|
|
|
251
251
|
measureList.forEach((staves) => {
|
|
252
252
|
const arr = staves ?? [];
|
|
253
253
|
let mx0 = Infinity, mx1 = -Infinity, nsx = Infinity;
|
|
254
|
-
const
|
|
254
|
+
const byTs = /* @__PURE__ */ new Map();
|
|
255
255
|
for (const m of arr) {
|
|
256
256
|
const mb = toBox(m?.PositionAndShape);
|
|
257
257
|
if (mb) {
|
|
@@ -268,13 +268,19 @@ function extractGeometry(osmd, canvas, inkSumThreshold) {
|
|
|
268
268
|
(gn) => !(gn?.sourceNote?.isRestFlag ?? gn?.sourceNote?.IsRest ?? false)
|
|
269
269
|
)
|
|
270
270
|
);
|
|
271
|
-
if (hasNote)
|
|
271
|
+
if (!hasNote) continue;
|
|
272
|
+
const tsRaw = se?.relInMeasureTimestamp?.RealValue ?? se?.getAbsoluteTimestamp?.()?.RealValue ?? se?.sourceStaffEntry?.Timestamp?.RealValue;
|
|
273
|
+
const key = typeof tsRaw === "number" ? Math.round(tsRaw * 1e4) : Math.round(sx * f);
|
|
274
|
+
const px = sx * f;
|
|
275
|
+
const prev = byTs.get(key);
|
|
276
|
+
if (prev === void 0 || px < prev) byTs.set(key, px);
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
279
|
if (!Number.isFinite(mx0) || mx1 - mx0 <= 1) return;
|
|
275
280
|
const startX = Number.isFinite(nsx) ? Math.max(mx0, Math.min(nsx, mx1)) : mx0;
|
|
276
281
|
const denom = mx1 - startX || 1;
|
|
277
|
-
for (const
|
|
282
|
+
for (const key of [...byTs.keys()].sort((a, b) => a - b)) {
|
|
283
|
+
const x = byTs.get(key);
|
|
278
284
|
noteCols.push(ordinal + Math.min(1, Math.max(0, (x - startX) / denom)));
|
|
279
285
|
}
|
|
280
286
|
ordinal++;
|
package/dist/promo.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/promo.ts"],"sourcesContent":["// Promo utilities — browser-only except for parseMidi/midiDurationMs which are\n// pure (no DOM).\n//\n// Exports:\n// parseMidi / midiDurationMs — pure MIDI parser (no DOM; testable in Node)\n// renderNotation — OSMD canvas renderer (browser/OSMD only)\n// createPromoSampler — Tone.js sampler factory (browser/Tone only)\n//\n// Unit tests cover parseMidi (tests/midi.test.ts).\n// renderNotation and createPromoSampler are browser-only — consumers' dom tests\n// cover them (OSMD and Tone.js require a browser context).\n\n// ─── MIDI parser ──────────────────────────────────────────────────────────────\n// Moved verbatim from realmusictheory/site/src/lib/promo/midiDuration.ts.\n\nexport interface MidiNote {\n midi: number;\n startMs: number;\n /** Audible duration in ms — extended while the sustain pedal is held. */\n durMs: number;\n /** 0..1 */\n velocity: number;\n}\n\nexport interface ParsedMidi {\n /** When the last notated note is released (pre-pedal-tail), in ms — paces the playhead. */\n durationMs: number;\n notes: MidiNote[];\n}\n\ninterface RawEvent {\n tick: number;\n kind: 'on' | 'off' | 'sustain' | 'tempo';\n midi?: number;\n velocity?: number;\n on?: boolean; // sustain down?\n us?: number; // tempo in µs/beat\n}\n\nexport function parseMidi(buf: ArrayBuffer): ParsedMidi {\n try {\n const dv = new DataView(buf);\n let p = 0;\n const u8 = () => dv.getUint8(p++);\n const u16 = () => { const v = dv.getUint16(p); p += 2; return v; };\n const u32 = () => { const v = dv.getUint32(p); p += 4; return v; };\n\n if (u32() !== 0x4d546864) return { durationMs: 0, notes: [] }; // 'MThd'\n const headerLen = u32();\n u16(); // format\n const ntrk = u16();\n const division = u16();\n p = 8 + headerLen;\n if (division & 0x8000) return { durationMs: 0, notes: [] }; // SMPTE — not handled\n const tpq = division || 480;\n\n const events: RawEvent[] = [];\n for (let t = 0; t < ntrk; t++) {\n if (p + 8 > dv.byteLength || u32() !== 0x4d54726b) break; // 'MTrk'\n const len = u32();\n const end = Math.min(p + len, dv.byteLength);\n let tick = 0;\n let running = 0;\n while (p < end) {\n let dt = 0, b: number;\n do { b = u8(); dt = (dt << 7) | (b & 0x7f); } while (b & 0x80 && p < end);\n tick += dt;\n let status = dv.getUint8(p);\n if (status & 0x80) { p++; running = status; } else { status = running; }\n if (status === 0xff) {\n const type = u8();\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n if (type === 0x51 && l === 3) {\n events.push({\n tick,\n kind: 'tempo',\n us: (dv.getUint8(p) << 16) | (dv.getUint8(p + 1) << 8) | dv.getUint8(p + 2),\n });\n }\n p += l;\n } else if (status === 0xf0 || status === 0xf7) {\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n p += l;\n } else {\n const hi = status & 0xf0;\n if (hi === 0x90 || hi === 0x80) {\n const midi = u8();\n const vel = u8();\n if (hi === 0x90 && vel > 0) events.push({ tick, kind: 'on', midi, velocity: vel / 127 });\n else events.push({ tick, kind: 'off', midi });\n } else if (hi === 0xb0) {\n const cc = u8();\n const val = u8();\n if (cc === 64) events.push({ tick, kind: 'sustain', on: val >= 64 });\n } else {\n p += hi === 0xc0 || hi === 0xd0 ? 1 : 2;\n }\n }\n }\n p = end;\n }\n\n // tick → ms via the tempo map\n const tempos = events.filter((e) => e.kind === 'tempo').sort((a, b) => a.tick - b.tick);\n if (!tempos.length || tempos[0].tick > 0) tempos.unshift({ tick: 0, kind: 'tempo', us: 500000 });\n const tickToMs = (tick: number): number => {\n let ms = 0;\n for (let i = 0; i < tempos.length; i++) {\n const segStart = tempos[i].tick;\n if (segStart >= tick) break;\n const segEnd = i + 1 < tempos.length ? Math.min(tempos[i + 1].tick, tick) : tick;\n ms += ((segEnd - segStart) / tpq) * ((tempos[i].us ?? 500000) / 1000);\n }\n return ms;\n };\n\n // Sustain spans (tick ranges where the pedal is down)\n const sustainEvents = events.filter((e) => e.kind === 'sustain').sort((a, b) => a.tick - b.tick);\n const pedalUpAfter = (tick: number): number | null => {\n for (const s of sustainEvents) if (!s.on && s.tick >= tick) return s.tick;\n return null;\n };\n const pedalDownAt = (tick: number): boolean => {\n let down = false;\n for (const s of sustainEvents) { if (s.tick > tick) break; down = !!s.on; }\n return down;\n };\n\n // Pair note-ons with the next matching note-off\n const ordered = events.filter((e) => e.kind === 'on' || e.kind === 'off').sort((a, b) => a.tick - b.tick);\n const open: Record<number, { tick: number; vel: number }[]> = {};\n const notes: MidiNote[] = [];\n let lastOffTick = 0;\n for (const e of ordered) {\n const m = e.midi!;\n if (e.kind === 'on') {\n (open[m] ??= []).push({ tick: e.tick, vel: e.velocity ?? 0.7 });\n } else {\n const stack = open[m];\n if (stack && stack.length) {\n const start = stack.shift()!;\n let endTick = e.tick;\n lastOffTick = Math.max(lastOffTick, endTick);\n // Extend while pedal is held past the note-off.\n if (pedalDownAt(endTick)) {\n const up = pedalUpAfter(endTick);\n if (up != null) endTick = up;\n }\n const startMs = tickToMs(start.tick);\n notes.push({\n midi: m,\n startMs,\n durMs: Math.max(60, tickToMs(endTick) - startMs),\n velocity: start.vel,\n });\n }\n }\n }\n\n return { durationMs: tickToMs(lastOffTick), notes };\n } catch {\n return { durationMs: 0, notes: [] };\n }\n}\n\n/** Total playback duration in ms (0 if unparseable). */\nexport function midiDurationMs(buf: ArrayBuffer): number {\n return parseMidi(buf).durationMs;\n}\n\n// ─── Notation renderer ────────────────────────────────────────────────────────\n// Superset of RSR (stave-web-sightread/src/lib/promo/notation.ts) and RMT\n// (realmusictheory/site/src/lib/promo/notation.ts). Both per-staff measure boxes\n// (RSR) and per-measure column union boxes (RMT) are computed every render.\n//\n// Browser-only: requires document + opensheetmusicdisplay. No unit tests here —\n// consumers' dom tests cover renderNotation.\n\nexport interface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/** One staff's slice of a measure, in canvas px. */\nexport interface StaffMeasureBox {\n index: number; // 0-based measure position within the rendered range\n staff: number; // 0 = top staff (RH/treble), 1 = bottom staff (LH/bass)\n box: Box;\n /** x of the measure's FIRST note (canvas px) — past any clef/key/time signature. */\n noteStartX: number;\n}\n\n/** Per-measure column box: union across all staves for that measure, in canvas px. */\nexport interface MeasureColumnBox extends Box {\n noteStartX: number;\n}\n\nexport interface RenderedNotation {\n canvas: HTMLCanvasElement;\n /** Per-row grand-staff system boxes in canvas px, top-to-bottom. */\n systems: Box[];\n /** Per-(measure, staff) boxes — RSR's geometry. */\n measures: StaffMeasureBox[];\n /** Per-measure column union across staves — RMT's geometry. */\n measureColumns: MeasureColumnBox[];\n /** Tight bounding box of all notation in canvas px (page whitespace cropped). */\n content: Box;\n /**\n * Per-distinct-onset engraved column position (measureIndex + fraction across\n * the note region), in time (left→right) order — one entry per played onset,\n * rests excluded, chords/both-hands collapsed. The scroll-cursor anchors to\n * THESE (the notes' real engraved X) instead of a time-fraction, so the\n * playhead lands exactly on each notehead. Empty/absent when extraction fails\n * or the engraving predates this field (cursor falls back to time/ordinal).\n */\n noteCols?: number[];\n}\n\nexport interface RenderNotationOpts {\n /** OSMD drawFrom/drawUpToMeasureNumber. Applied only when provided. */\n bars?: [number, number];\n /** Background fill colour. Default '#faf7f0' (RSR paper). */\n paper?: string;\n /** Pixel-sum threshold below which a pixel is counted as ink.\n * Default 690 (RSR: paper #faf7f0 sum ≈ 737). RMT uses 620. */\n inkSumThreshold?: number;\n /** Host div width in CSS px. Default 560 (RSR). RMT uses 620. */\n hostWidth?: number;\n /**\n * Notation scroll mode — drives BOTH the engraving layout here and the\n * follow-camera in the scroll-cursor/notation layers.\n *\n * 'hstack' (default) — engrave all measures on ONE horizontal staffline\n * (OSMD RenderSingleHorizontalStaffline). The follow window is a purely\n * HORIZONTAL slice so the playhead pans left→right with no vertical\n * row-break jump. The rasterized bitmap is one very wide row; geometry\n * (systems/measures) all share one row of y.\n *\n * 'vstack' — engrave the classic STACKED systems (normal page wrap into\n * multiple rows). The follow camera frames the ACTIVE system at a fixed\n * band position and scrolls VERTICALLY (eased) to the next system as the\n * playhead crosses systems — never a vertical leap across staff lines.\n *\n * Default 'hstack' (preserves the current single-row behavior).\n */\n scrollMode?: 'hstack' | 'vstack';\n /**\n * @deprecated Use `scrollMode` instead. Back-compat alias: `singleRow:true`\n * ⇔ `scrollMode:'hstack'`, `singleRow:false` ⇔ `scrollMode:'vstack'`.\n * When both are given, `scrollMode` wins.\n */\n singleRow?: boolean;\n}\n\n/** Padded union of boxes, clamped to the canvas. */\nfunction unionBox(boxes: Box[], canvas: HTMLCanvasElement, pad: number): Box {\n if (!boxes.length) return { x: 0, y: 0, w: canvas.width, h: canvas.height };\n const minX = Math.min(...boxes.map((b) => b.x));\n const minY = Math.min(...boxes.map((b) => b.y));\n const maxX = Math.max(...boxes.map((b) => b.x + b.w));\n const maxY = Math.max(...boxes.map((b) => b.y + b.h));\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(canvas.width, maxX + pad) - x,\n h: Math.min(canvas.height, maxY + pad) - y,\n };\n}\n\n/** Tight box around all non-paper pixels (notes, ledger lines, stems), padded. */\nfunction inkBoundingBox(canvas: HTMLCanvasElement, inkSumThreshold: number): Box | null {\n try {\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return null;\n const { width: W, height: H } = canvas;\n const data = ctx.getImageData(0, 0, W, H).data;\n let minX = W, minY = H, maxX = -1, maxY = -1;\n const step = 2;\n for (let y = 0; y < H; y += step) {\n for (let x = 0; x < W; x += step) {\n const i = (y * W + x) * 4;\n if (data[i + 3] < 16) continue;\n if (data[i] + data[i + 1] + data[i + 2] < inkSumThreshold) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n if (maxX < 0) return null;\n const pad = 16;\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(W, maxX + pad) - x,\n h: Math.min(H, maxY + pad) - y,\n };\n } catch {\n return null;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nfunction extractGeometry(\n osmd: any,\n canvas: HTMLCanvasElement,\n inkSumThreshold: number,\n): { systems: Box[]; measures: StaffMeasureBox[]; measureColumns: MeasureColumnBox[]; content: Box; noteCols: number[] } {\n const full: Box = { x: 0, y: 0, w: canvas.width, h: canvas.height };\n try {\n const graphic: any = osmd.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageW: number = page?.PositionAndShape?.Size?.width;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!pageW || !musicSystems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n // OSMD units → canvas px\n const f = canvas.width / pageW;\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n\n // RSR geometry: per-(measure, staff) individual boxes\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n // RMT geometry: per-measure column boxes (union across staves)\n const measureColumns: MeasureColumnBox[] = measureList\n .map((staves) => {\n const arr = staves ?? [];\n const boxes = arr\n .map((m: any) => toBox(m?.PositionAndShape))\n .filter((b: Box | null): b is Box => !!b && b.w > 1 && b.h > 1);\n if (!boxes.length) return null;\n const x0 = Math.min(...boxes.map((b) => b.x));\n const y0 = Math.min(...boxes.map((b) => b.y));\n const x1 = Math.max(...boxes.map((b) => b.x + b.w));\n const y1 = Math.max(...boxes.map((b) => b.y + b.h));\n // First note x across all staves; clamped into the column box\n let nx = Infinity;\n for (const m of arr) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof seX === 'number') nx = Math.min(nx, seX * f);\n }\n const noteStartX = Number.isFinite(nx) ? Math.max(x0, Math.min(nx, x1)) : x0;\n return { x: x0, y: y0, w: x1 - x0, h: y1 - y0, noteStartX };\n })\n .filter((b): b is MeasureColumnBox => !!b);\n\n // Per-distinct-onset engraved columns, in render (= time) order, rests excluded\n // — the cursor's TRUE anchor X. Each value is `ordinal + frac`, where `ordinal`\n // is the column's position among the DRAWN measures (matching the array index of\n // measureColumnsFromLayout — NOT the absolute MeasureList index, which would\n // overflow the columns array for excerpts whose first measure isn't bar 0), and\n // `frac` is the notehead's position across the measure's note region. Crucially\n // we iterate ONLY drawn measures (a valid box): with drawFrom/drawUpTo, OSMD's\n // MeasureList still holds EVERY source measure, so without this filter the whole\n // score leaks in (e.g. 128 cols for a 22-note excerpt) and never pairs 1:1 with\n // the played onsets, disabling the engraved-x anchor.\n const noteCols: number[] = [];\n let ordinal = 0;\n measureList.forEach((staves) => {\n const arr = staves ?? [];\n let mx0 = Infinity, mx1 = -Infinity, nsx = Infinity;\n const xs: number[] = [];\n for (const m of arr) {\n const mb = toBox(m?.PositionAndShape);\n if (mb) { mx0 = Math.min(mx0, mb.x); mx1 = Math.max(mx1, mb.x + mb.w); }\n const se0 = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof se0 === 'number') nsx = Math.min(nsx, se0 * f);\n for (const se of (m?.staffEntries ?? [])) {\n const sx = se?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof sx !== 'number') continue;\n const hasNote = (se?.graphicalVoiceEntries ?? []).some(\n (gve: any) => (gve?.notes ?? []).some(\n (gn: any) => !(gn?.sourceNote?.isRestFlag ?? gn?.sourceNote?.IsRest ?? false)));\n if (hasNote) xs.push(sx * f);\n }\n }\n // Skip undrawn measures (no valid box / zero width) — only drawn measures get\n // a column, so `ordinal` stays in lockstep with measureColumnsFromLayout.\n if (!Number.isFinite(mx0) || (mx1 - mx0) <= 1) return;\n const startX = Number.isFinite(nsx) ? Math.max(mx0, Math.min(nsx, mx1)) : mx0;\n const denom = (mx1 - startX) || 1;\n for (const x of [...new Set(xs.map((v) => Math.round(v)))].sort((a, b) => a - b)) {\n noteCols.push(ordinal + Math.min(1, Math.max(0, (x - startX) / denom)));\n }\n ordinal++;\n });\n\n const content = inkBoundingBox(canvas, inkSumThreshold) ?? unionBox(systems, canvas, 14);\n return { systems, measures, measureColumns, content, noteCols };\n } catch {\n return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/** Render a MusicXML string to a detached canvas + geometry.\n * Browser-only (requires document + opensheetmusicdisplay dynamic import). */\nexport async function renderNotation(\n xml: string,\n opts?: RenderNotationOpts,\n): Promise<RenderedNotation> {\n const paper = opts?.paper ?? '#faf7f0';\n const inkSumThreshold = opts?.inkSumThreshold ?? 690;\n const hostWidth = opts?.hostWidth ?? 560;\n // scrollMode drives the engraving layout; singleRow is the deprecated alias.\n // Default 'hstack' (single horizontal staffline) preserves current behavior.\n const scrollMode: 'hstack' | 'vstack' =\n opts?.scrollMode ?? (opts?.singleRow === false ? 'vstack' : 'hstack');\n const singleRow = scrollMode === 'hstack';\n\n // Literal dynamic import: consumers' bundlers (Vite/Rollup) must be able to\n // statically see the specifier to resolve + code-split it — a variable\n // specifier would reach the browser as a bare import and fail at runtime.\n const { OpenSheetMusicDisplay } = await import('opensheetmusicdisplay');\n\n const host = document.createElement('div');\n host.style.cssText = `position:fixed;left:-9999px;top:0;width:${hostWidth}px;background:${paper};`;\n document.body.appendChild(host);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const osmd: any = new OpenSheetMusicDisplay(host, {\n backend: 'canvas',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n\n await osmd.load(xml);\n\n // hstack: engrave on a single horizontal staffline so the follow window pans\n // purely HORIZONTALLY (no stacked-system row breaks → no vertical playhead\n // jump). vstack: leave OSMD's default page wrap → classic stacked systems\n // (the follow camera then scrolls vertically between systems). Must be set\n // before render(); when on, the rasterized canvas becomes one wide row.\n if (singleRow) {\n osmd.setOptions({ renderSingleHorizontalStaffline: true } as never);\n }\n\n // Apply bar range only when provided (RSR's drawFrom/drawUpTo).\n if (opts?.bars) {\n osmd.setOptions({\n drawFromMeasureNumber: opts.bars[0],\n drawUpToMeasureNumber: opts.bars[1],\n } as never);\n }\n\n osmd.render();\n\n const canvas = host.querySelector('canvas');\n if (canvas) {\n const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);\n document.body.removeChild(host);\n return { canvas, systems, measures, measureColumns, content, noteCols };\n }\n\n // SVG fallback: rasterize into a canvas so callers always get a canvas.\n const svg = host.querySelector('svg');\n if (svg) {\n const w = svg.clientWidth || hostWidth;\n const h = svg.clientHeight || 300;\n const svgStr = new XMLSerializer().serializeToString(svg);\n const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error('svg rasterize failed'));\n img.src = dataUrl;\n });\n const out = document.createElement('canvas');\n out.width = w;\n out.height = h;\n const c2d = out.getContext('2d');\n if (!c2d) throw new Error('2d context unavailable');\n c2d.fillStyle = paper;\n c2d.fillRect(0, 0, w, h);\n c2d.drawImage(img, 0, 0, w, h);\n document.body.removeChild(host);\n return {\n canvas: out,\n systems: [],\n measures: [],\n measureColumns: [],\n content: { x: 0, y: 0, w, h },\n noteCols: [],\n };\n }\n\n document.body.removeChild(host);\n throw new Error('OSMD produced neither canvas nor SVG');\n } catch (e) {\n if (host.parentNode) document.body.removeChild(host);\n throw e;\n }\n}\n\n// ─── Promo sampler ────────────────────────────────────────────────────────────\n// The shared Tone.js setup prefix under both apps' createPromoAudio.\n// Scheduling (playEvents / playMidi / playWindowSolo / playForeground) stays\n// in each app.\n//\n// Browser-only: requires Tone.js dynamic import. No unit tests here —\n// consumers' dom tests cover createPromoSampler.\n\nimport { createSalamanderSampler, generateReverb } from './audioHelpers';\nimport { SALAMANDER_CDN_BASE, SALAMANDER_URLS_8, SALAMANDER_URLS_FULL } from './salamander';\n\nexport interface PromoSampler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Tone: any; // typeof Tone — typed loose like audioHelpers.ts does for Tone\n sampler: unknown; // Tone.Sampler; typed loose like audioHelpers\n getStream(): MediaStream;\n /** Master-bus analyser (post-chain, sink-only) for reactive visualizers, or\n * null if `analyser` wasn't requested. whozart's \"listen\" spectrum reads it. */\n getAnalyser(): AnalyserNode | null;\n /** Current audio-clock time in seconds (Tone.now()). */\n audioNow(): number;\n /** releaseAll on the sampler; safe no-op on failure. */\n stop(): void;\n}\n\n/** Per-app tone shaping for the shared promo mastering bus. Ported from\n * whozart's audio-impl chain (the richest of the fleet); `reading` is a drier,\n * closer voicing so sight-reading notation stays legible note-to-note. Apps\n * pick one via `createPromoSampler({ voicing })`. */\nexport type PromoVoicing = 'concertHall' | 'reading' | 'dry';\n\ninterface VoicingPreset {\n release: number;\n attack: number;\n eq: { low: number; mid: number; high: number; lowFrequency: number; highFrequency: number };\n comp: { threshold: number; ratio: number; attack: number; release: number; knee: number };\n widen: number;\n reverb: { decay: number; preDelay: number; wet: number };\n}\n\nconst PROMO_VOICINGS: Record<PromoVoicing, VoicingPreset> = {\n // whozart's hall sound: long ringing release, lifted lows/air, gentle glue\n // compression, wide stereo, a real-hall reverb tail.\n concertHall: {\n release: 1.8, attack: 0.005,\n eq: { low: 1.5, mid: -1, high: 1.5, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -18, ratio: 1.6, attack: 0.012, release: 0.3, knee: 18 },\n widen: 0.30,\n reverb: { decay: 3.5, preDelay: 0.04, wet: 0.22 },\n },\n // Sight-reading: shorter release + much drier/shorter reverb so consecutive\n // notes don't smear into each other while the eye tracks the staff. Small\n // presence bump in the mids for note-attack clarity, narrower stereo.\n reading: {\n release: 1.1, attack: 0.004,\n eq: { low: 0.5, mid: 1, high: 1, lowFrequency: 280, highFrequency: 5000 },\n comp: { threshold: -16, ratio: 2.0, attack: 0.008, release: 0.25, knee: 12 },\n widen: 0.18,\n reverb: { decay: 1.4, preDelay: 0.02, wet: 0.10 },\n },\n // Bone-dry — no reverb at all (e.g. a debugging/reference voicing).\n dry: {\n release: 1.0, attack: 0.004,\n eq: { low: 0, mid: 0, high: 0, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -14, ratio: 2.0, attack: 0.008, release: 0.25, knee: 10 },\n widen: 0,\n reverb: { decay: 1.0, preDelay: 0, wet: 0 },\n },\n};\n\nexport async function createPromoSampler(opts?: {\n /** Feed a 0-value ConstantSource into the capture stream so the recorder's\n * audio track is live from t=0 (silent intro scenes aren't dropped).\n * Default false (RSR behavior). RMT passes true. */\n keepAlive?: boolean;\n /** Override the voicing's sampler release time (seconds). */\n release?: number;\n /** Sampler volume in dB. Default -2. */\n volumeDb?: number;\n /** Per-app tone shaping (see PromoVoicing). Default 'concertHall'. */\n voicing?: PromoVoicing;\n /** Use the full A0..C8 Salamander anchor set instead of the 8-anchor set —\n * fuller tone, more samples to fetch. Default false (8-anchor). */\n fullSamples?: boolean;\n /** Expose a master-bus analyser (for reactive visualizers). Default false. */\n analyser?: boolean;\n}): Promise<PromoSampler> {\n const voicing = PROMO_VOICINGS[opts?.voicing ?? 'concertHall'];\n const release = opts?.release ?? voicing.release;\n const volumeDb = opts?.volumeDb ?? -2;\n const keepAlive = opts?.keepAlive ?? false;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const Tone = (await import('tone')) as any;\n await Tone.start();\n\n // Unrouted sampler — we build the mastering bus off it (don't send the dry\n // sampler to the speakers, which would bypass the chain).\n const sampler = createSalamanderSampler(Tone, {\n urls: opts?.fullSamples ? SALAMANDER_URLS_FULL : SALAMANDER_URLS_8,\n baseUrl: SALAMANDER_CDN_BASE,\n release,\n attack: voicing.attack,\n volumeDb,\n connectToDestination: false,\n });\n\n // Mastering bus (ported from whozart): EQ tilt → glue compressor → stereo\n // widener → reverb → brick-wall limiter. Reverb is generated with a timeout\n // fallback so a slow/again-failing IR can never hang the render; on failure\n // the chain simply omits reverb.\n const eq = new Tone.EQ3(voicing.eq);\n const compressor = new Tone.Compressor(voicing.comp);\n const widener = new Tone.StereoWidener(voicing.widen);\n const limiter = new Tone.Limiter(-1.5);\n\n const reverb =\n voicing.reverb.wet > 0\n ? await generateReverb(\n Tone,\n { decay: voicing.reverb.decay, wet: voicing.reverb.wet },\n 4000,\n )\n : null;\n if (reverb) reverb.preDelay = voicing.reverb.preDelay;\n\n // chain() wires node→node in order; tail node feeds the taps below.\n const chainNodes = reverb ? [eq, compressor, widener, reverb, limiter] : [eq, compressor, widener, limiter];\n sampler.chain(...chainNodes);\n\n const ctx = Tone.getContext().rawContext as AudioContext;\n const mediaDest = ctx.createMediaStreamDestination();\n limiter.connect(mediaDest);\n // Also monitor through the speakers so a non-headless preview is audible.\n limiter.connect(Tone.getDestination());\n\n // Optional master-bus analyser (sink-only — no onward connection so it can't\n // double the signal). Drives whozart's reactive \"listen\" visualizer.\n let analyserNode: AnalyserNode | null = null;\n if (opts?.analyser) {\n analyserNode = ctx.createAnalyser();\n analyserNode.fftSize = 2048;\n analyserNode.smoothingTimeConstant = 0.8;\n limiter.connect(analyserNode);\n }\n\n // Optional: keep the audio track alive from t=0 so the recorder doesn't drop\n // silent intro scenes. RMT uses this; RSR does not.\n if (keepAlive) {\n const source = ctx.createConstantSource();\n source.offset.value = 0;\n source.connect(mediaDest);\n source.start();\n }\n\n await Tone.loaded();\n\n return {\n Tone,\n sampler,\n getStream(): MediaStream {\n return mediaDest.stream;\n },\n getAnalyser(): AnalyserNode | null {\n return analyserNode;\n },\n audioNow(): number {\n return Tone.now();\n },\n stop(): void {\n try {\n (sampler as unknown as { releaseAll?: () => void }).releaseAll?.();\n } catch {\n /* no-op */\n }\n },\n };\n}\n"],"mappings":";;;;;;;;;AAuCO,SAAS,UAAU,KAA8B;AACtD,MAAI;AACF,UAAM,KAAK,IAAI,SAAS,GAAG;AAC3B,QAAI,IAAI;AACR,UAAM,KAAK,MAAM,GAAG,SAAS,GAAG;AAChC,UAAM,MAAM,MAAM;AAAE,YAAM,IAAI,GAAG,UAAU,CAAC;AAAG,WAAK;AAAG,aAAO;AAAA,IAAG;AACjE,UAAM,MAAM,MAAM;AAAE,YAAM,IAAI,GAAG,UAAU,CAAC;AAAG,WAAK;AAAG,aAAO;AAAA,IAAG;AAEjE,QAAI,IAAI,MAAM,WAAY,QAAO,EAAE,YAAY,GAAG,OAAO,CAAC,EAAE;AAC5D,UAAM,YAAY,IAAI;AACtB,QAAI;AACJ,UAAM,OAAO,IAAI;AACjB,UAAM,WAAW,IAAI;AACrB,QAAI,IAAI;AACR,QAAI,WAAW,MAAQ,QAAO,EAAE,YAAY,GAAG,OAAO,CAAC,EAAE;AACzD,UAAM,MAAM,YAAY;AAExB,UAAM,SAAqB,CAAC;AAC5B,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,UAAI,IAAI,IAAI,GAAG,cAAc,IAAI,MAAM,WAAY;AACnD,YAAM,MAAM,IAAI;AAChB,YAAM,MAAM,KAAK,IAAI,IAAI,KAAK,GAAG,UAAU;AAC3C,UAAI,OAAO;AACX,UAAI,UAAU;AACd,aAAO,IAAI,KAAK;AACd,YAAI,KAAK,GAAG;AACZ,WAAG;AAAE,cAAI,GAAG;AAAG,eAAM,MAAM,IAAM,IAAI;AAAA,QAAO,SAAS,IAAI,OAAQ,IAAI;AACrE,gBAAQ;AACR,YAAI,SAAS,GAAG,SAAS,CAAC;AAC1B,YAAI,SAAS,KAAM;AAAE;AAAK,oBAAU;AAAA,QAAQ,OAAO;AAAE,mBAAS;AAAA,QAAS;AACvE,YAAI,WAAW,KAAM;AACnB,gBAAM,OAAO,GAAG;AAChB,cAAI,IAAI,GAAG;AACX,aAAG;AAAE,iBAAK,GAAG;AAAG,gBAAK,KAAK,IAAM,KAAK;AAAA,UAAO,SAAS,KAAK,OAAQ,IAAI;AACtE,cAAI,SAAS,MAAQ,MAAM,GAAG;AAC5B,mBAAO,KAAK;AAAA,cACV;AAAA,cACA,MAAM;AAAA,cACN,IAAK,GAAG,SAAS,CAAC,KAAK,KAAO,GAAG,SAAS,IAAI,CAAC,KAAK,IAAK,GAAG,SAAS,IAAI,CAAC;AAAA,YAC5E,CAAC;AAAA,UACH;AACA,eAAK;AAAA,QACP,WAAW,WAAW,OAAQ,WAAW,KAAM;AAC7C,cAAI,IAAI,GAAG;AACX,aAAG;AAAE,iBAAK,GAAG;AAAG,gBAAK,KAAK,IAAM,KAAK;AAAA,UAAO,SAAS,KAAK,OAAQ,IAAI;AACtE,eAAK;AAAA,QACP,OAAO;AACL,gBAAM,KAAK,SAAS;AACpB,cAAI,OAAO,OAAQ,OAAO,KAAM;AAC9B,kBAAM,OAAO,GAAG;AAChB,kBAAM,MAAM,GAAG;AACf,gBAAI,OAAO,OAAQ,MAAM,EAAG,QAAO,KAAK,EAAE,MAAM,MAAM,MAAM,MAAM,UAAU,MAAM,IAAI,CAAC;AAAA,gBAClF,QAAO,KAAK,EAAE,MAAM,MAAM,OAAO,KAAK,CAAC;AAAA,UAC9C,WAAW,OAAO,KAAM;AACtB,kBAAM,KAAK,GAAG;AACd,kBAAM,MAAM,GAAG;AACf,gBAAI,OAAO,GAAI,QAAO,KAAK,EAAE,MAAM,MAAM,WAAW,IAAI,OAAO,GAAG,CAAC;AAAA,UACrE,OAAO;AACL,iBAAK,OAAO,OAAQ,OAAO,MAAO,IAAI;AAAA,UACxC;AAAA,QACF;AAAA,MACF;AACA,UAAI;AAAA,IACN;AAGA,UAAM,SAAS,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AACtF,QAAI,CAAC,OAAO,UAAU,OAAO,CAAC,EAAE,OAAO,EAAG,QAAO,QAAQ,EAAE,MAAM,GAAG,MAAM,SAAS,IAAI,IAAO,CAAC;AAC/F,UAAM,WAAW,CAAC,SAAyB;AACzC,UAAI,KAAK;AACT,eAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,cAAM,WAAW,OAAO,CAAC,EAAE;AAC3B,YAAI,YAAY,KAAM;AACtB,cAAM,SAAS,IAAI,IAAI,OAAO,SAAS,KAAK,IAAI,OAAO,IAAI,CAAC,EAAE,MAAM,IAAI,IAAI;AAC5E,eAAQ,SAAS,YAAY,QAAS,OAAO,CAAC,EAAE,MAAM,OAAU;AAAA,MAClE;AACA,aAAO;AAAA,IACT;AAGA,UAAM,gBAAgB,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AAC/F,UAAM,eAAe,CAAC,SAAgC;AACpD,iBAAW,KAAK,cAAe,KAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,KAAM,QAAO,EAAE;AACrE,aAAO;AAAA,IACT;AACA,UAAM,cAAc,CAAC,SAA0B;AAC7C,UAAI,OAAO;AACX,iBAAW,KAAK,eAAe;AAAE,YAAI,EAAE,OAAO,KAAM;AAAO,eAAO,CAAC,CAAC,EAAE;AAAA,MAAI;AAC1E,aAAO;AAAA,IACT;AAGA,UAAM,UAAU,OAAO,OAAO,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,SAAS,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,EAAE,IAAI;AACxG,UAAM,OAAwD,CAAC;AAC/D,UAAM,QAAoB,CAAC;AAC3B,QAAI,cAAc;AAClB,eAAW,KAAK,SAAS;AACvB,YAAM,IAAI,EAAE;AACZ,UAAI,EAAE,SAAS,MAAM;AACnB,SAAC,KAAK,CAAC,MAAM,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,EAAE,YAAY,IAAI,CAAC;AAAA,MAChE,OAAO;AACL,cAAM,QAAQ,KAAK,CAAC;AACpB,YAAI,SAAS,MAAM,QAAQ;AACzB,gBAAM,QAAQ,MAAM,MAAM;AAC1B,cAAI,UAAU,EAAE;AAChB,wBAAc,KAAK,IAAI,aAAa,OAAO;AAE3C,cAAI,YAAY,OAAO,GAAG;AACxB,kBAAM,KAAK,aAAa,OAAO;AAC/B,gBAAI,MAAM,KAAM,WAAU;AAAA,UAC5B;AACA,gBAAM,UAAU,SAAS,MAAM,IAAI;AACnC,gBAAM,KAAK;AAAA,YACT,MAAM;AAAA,YACN;AAAA,YACA,OAAO,KAAK,IAAI,IAAI,SAAS,OAAO,IAAI,OAAO;AAAA,YAC/C,UAAU,MAAM;AAAA,UAClB,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,WAAO,EAAE,YAAY,SAAS,WAAW,GAAG,MAAM;AAAA,EACpD,QAAQ;AACN,WAAO,EAAE,YAAY,GAAG,OAAO,CAAC,EAAE;AAAA,EACpC;AACF;AAGO,SAAS,eAAe,KAA0B;AACvD,SAAO,UAAU,GAAG,EAAE;AACxB;AAyFA,SAAS,SAAS,OAAc,QAA2B,KAAkB;AAC3E,MAAI,CAAC,MAAM,OAAQ,QAAO,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,OAAO,OAAO,GAAG,OAAO,OAAO;AAC1E,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9C,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC9C,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AACpD,QAAM,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG;AAChC,QAAM,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG;AAChC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,GAAG,KAAK,IAAI,OAAO,OAAO,OAAO,GAAG,IAAI;AAAA,IACxC,GAAG,KAAK,IAAI,OAAO,QAAQ,OAAO,GAAG,IAAI;AAAA,EAC3C;AACF;AAGA,SAAS,eAAe,QAA2B,iBAAqC;AACtF,MAAI;AACF,UAAM,MAAM,OAAO,WAAW,MAAM,EAAE,oBAAoB,KAAK,CAAC;AAChE,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,EAAE,OAAO,GAAG,QAAQ,EAAE,IAAI;AAChC,UAAM,OAAO,IAAI,aAAa,GAAG,GAAG,GAAG,CAAC,EAAE;AAC1C,QAAI,OAAO,GAAG,OAAO,GAAG,OAAO,IAAI,OAAO;AAC1C,UAAM,OAAO;AACb,aAASA,KAAI,GAAGA,KAAI,GAAGA,MAAK,MAAM;AAChC,eAASC,KAAI,GAAGA,KAAI,GAAGA,MAAK,MAAM;AAChC,cAAM,KAAKD,KAAI,IAAIC,MAAK;AACxB,YAAI,KAAK,IAAI,CAAC,IAAI,GAAI;AACtB,YAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,iBAAiB;AACzD,cAAIA,KAAI,KAAM,QAAOA;AACrB,cAAIA,KAAI,KAAM,QAAOA;AACrB,cAAID,KAAI,KAAM,QAAOA;AACrB,cAAIA,KAAI,KAAM,QAAOA;AAAA,QACvB;AAAA,MACF;AAAA,IACF;AACA,QAAI,OAAO,EAAG,QAAO;AACrB,UAAM,MAAM;AACZ,UAAM,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG;AAChC,UAAM,IAAI,KAAK,IAAI,GAAG,OAAO,GAAG;AAChC,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,GAAG,KAAK,IAAI,GAAG,OAAO,GAAG,IAAI;AAAA,MAC7B,GAAG,KAAK,IAAI,GAAG,OAAO,GAAG,IAAI;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,gBACP,MACA,QACA,iBACuH;AACvH,QAAM,OAAY,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,OAAO,OAAO,GAAG,OAAO,OAAO;AAClE,MAAI;AACF,UAAM,UAAe,KAAK;AAC1B,UAAM,OAAY,SAAS,aAAa,CAAC;AACzC,UAAM,QAAgB,MAAM,kBAAkB,MAAM;AACpD,UAAM,eAAsB,MAAM,gBAAgB,CAAC;AACnD,QAAI,CAAC,SAAS,CAAC,aAAa,OAAQ,QAAO,EAAE,SAAS,CAAC,GAAG,UAAU,CAAC,GAAG,gBAAgB,CAAC,GAAG,SAAS,MAAM,UAAU,CAAC,EAAE;AAGxH,UAAM,IAAI,OAAO,QAAQ;AACzB,UAAM,QAAQ,CAAC,QAAyB;AACtC,YAAM,IAAI,KAAK;AACf,YAAM,KAAK,KAAK;AAChB,UAAI,CAAC,KAAK,CAAC,GAAI,QAAO;AACtB,aAAO,EAAE,GAAG,EAAE,IAAI,GAAG,GAAG,EAAE,IAAI,GAAG,GAAG,GAAG,QAAQ,GAAG,GAAG,GAAG,SAAS,EAAE;AAAA,IACrE;AAEA,UAAM,UAAiB,aACpB,IAAI,CAAC,MAAM,MAAM,GAAG,gBAAgB,CAAC,EACrC,OAAO,CAAC,MAAgB,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,EAAE,IAAI,CAAC,EACjD,KAAK,CAAC,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;AAC3B,QAAI,CAAC,QAAQ,OAAQ,QAAO,EAAE,SAAS,CAAC,GAAG,UAAU,CAAC,GAAG,gBAAgB,CAAC,GAAG,SAAS,MAAM,UAAU,CAAC,EAAE;AAEzG,UAAM,cAAuB,SAAS,eAAe,CAAC;AAGtD,UAAM,WAA8B,CAAC;AACrC,gBAAY,QAAQ,CAAC,QAAQ,UAAU;AACrC,OAAC,UAAU,CAAC,GAAG,QAAQ,CAAC,GAAQ,UAAkB;AAChD,cAAM,MAAM,MAAM,GAAG,gBAAgB;AACrC,YAAI,OAAO,IAAI,IAAI,KAAK,IAAI,IAAI,GAAG;AACjC,gBAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,kBAAkB,kBAAkB;AAC5E,gBAAM,aAAa,OAAO,QAAQ,WAAW,MAAM,IAAI,IAAI;AAC3D,mBAAS,KAAK,EAAE,OAAO,OAAO,KAAK,WAAW,CAAC;AAAA,QACjD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAGD,UAAM,iBAAqC,YACxC,IAAI,CAAC,WAAW;AACf,YAAM,MAAM,UAAU,CAAC;AACvB,YAAM,QAAQ,IACX,IAAI,CAAC,MAAW,MAAM,GAAG,gBAAgB,CAAC,EAC1C,OAAO,CAAC,MAA4B,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,EAAE,IAAI,CAAC;AAChE,UAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,YAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,YAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,YAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAClD,YAAM,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;AAElD,UAAI,KAAK;AACT,iBAAW,KAAK,KAAK;AACnB,cAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,kBAAkB,kBAAkB;AAC5E,YAAI,OAAO,QAAQ,SAAU,MAAK,KAAK,IAAI,IAAI,MAAM,CAAC;AAAA,MACxD;AACA,YAAM,aAAa,OAAO,SAAS,EAAE,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC,IAAI;AAC1E,aAAO,EAAE,GAAG,IAAI,GAAG,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,WAAW;AAAA,IAC5D,CAAC,EACA,OAAO,CAAC,MAA6B,CAAC,CAAC,CAAC;AAY3C,UAAM,WAAqB,CAAC;AAC5B,QAAI,UAAU;AACd,gBAAY,QAAQ,CAAC,WAAW;AAC9B,YAAM,MAAM,UAAU,CAAC;AACvB,UAAI,MAAM,UAAU,MAAM,WAAW,MAAM;AAC3C,YAAM,KAAe,CAAC;AACtB,iBAAW,KAAK,KAAK;AACnB,cAAM,KAAK,MAAM,GAAG,gBAAgB;AACpC,YAAI,IAAI;AAAE,gBAAM,KAAK,IAAI,KAAK,GAAG,CAAC;AAAG,gBAAM,KAAK,IAAI,KAAK,GAAG,IAAI,GAAG,CAAC;AAAA,QAAG;AACvE,cAAM,OAAO,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,kBAAkB,kBAAkB;AAC5E,YAAI,OAAO,QAAQ,SAAU,OAAM,KAAK,IAAI,KAAK,MAAM,CAAC;AACxD,mBAAW,MAAO,GAAG,gBAAgB,CAAC,GAAI;AACxC,gBAAM,KAAK,IAAI,kBAAkB,kBAAkB;AACnD,cAAI,OAAO,OAAO,SAAU;AAC5B,gBAAM,WAAW,IAAI,yBAAyB,CAAC,GAAG;AAAA,YAChD,CAAC,SAAc,KAAK,SAAS,CAAC,GAAG;AAAA,cAC/B,CAAC,OAAY,EAAE,IAAI,YAAY,cAAc,IAAI,YAAY,UAAU;AAAA,YAAM;AAAA,UAAC;AAClF,cAAI,QAAS,IAAG,KAAK,KAAK,CAAC;AAAA,QAC7B;AAAA,MACF;AAGA,UAAI,CAAC,OAAO,SAAS,GAAG,KAAM,MAAM,OAAQ,EAAG;AAC/C,YAAM,SAAS,OAAO,SAAS,GAAG,IAAI,KAAK,IAAI,KAAK,KAAK,IAAI,KAAK,GAAG,CAAC,IAAI;AAC1E,YAAM,QAAS,MAAM,UAAW;AAChC,iBAAW,KAAK,CAAC,GAAG,IAAI,IAAI,GAAG,IAAI,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,GAAG;AAChF,iBAAS,KAAK,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,IAAI,IAAI,UAAU,KAAK,CAAC,CAAC;AAAA,MACxE;AACA;AAAA,IACF,CAAC;AAED,UAAM,UAAU,eAAe,QAAQ,eAAe,KAAK,SAAS,SAAS,QAAQ,EAAE;AACvF,WAAO,EAAE,SAAS,UAAU,gBAAgB,SAAS,SAAS;AAAA,EAChE,QAAQ;AACN,WAAO,EAAE,SAAS,CAAC,GAAG,UAAU,CAAC,GAAG,gBAAgB,CAAC,GAAG,SAAS,MAAM,UAAU,CAAC,EAAE;AAAA,EACtF;AACF;AAKA,eAAsB,eACpB,KACA,MAC2B;AAC3B,QAAM,QAAQ,MAAM,SAAS;AAC7B,QAAM,kBAAkB,MAAM,mBAAmB;AACjD,QAAM,YAAY,MAAM,aAAa;AAGrC,QAAM,aACJ,MAAM,eAAe,MAAM,cAAc,QAAQ,WAAW;AAC9D,QAAM,YAAY,eAAe;AAKjC,QAAM,EAAE,sBAAsB,IAAI,MAAM,OAAO,uBAAuB;AAEtE,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,MAAM,UAAU,2CAA2C,SAAS,iBAAiB,KAAK;AAC/F,WAAS,KAAK,YAAY,IAAI;AAE9B,MAAI;AAEF,UAAM,OAAY,IAAI,sBAAsB,MAAM;AAAA,MAChD,SAAS;AAAA,MACT,YAAY;AAAA,MACZ,WAAW;AAAA,MACX,cAAc;AAAA,MACd,cAAc;AAAA,MACd,cAAc;AAAA,MACd,eAAe;AAAA,IACjB,CAAC;AAED,UAAM,KAAK,KAAK,GAAG;AAOnB,QAAI,WAAW;AACb,WAAK,WAAW,EAAE,iCAAiC,KAAK,CAAU;AAAA,IACpE;AAGA,QAAI,MAAM,MAAM;AACd,WAAK,WAAW;AAAA,QACd,uBAAuB,KAAK,KAAK,CAAC;AAAA,QAClC,uBAAuB,KAAK,KAAK,CAAC;AAAA,MACpC,CAAU;AAAA,IACZ;AAEA,SAAK,OAAO;AAEZ,UAAM,SAAS,KAAK,cAAc,QAAQ;AAC1C,QAAI,QAAQ;AACV,YAAM,EAAE,SAAS,UAAU,gBAAgB,SAAS,SAAS,IAAI,gBAAgB,MAAM,QAAQ,eAAe;AAC9G,eAAS,KAAK,YAAY,IAAI;AAC9B,aAAO,EAAE,QAAQ,SAAS,UAAU,gBAAgB,SAAS,SAAS;AAAA,IACxE;AAGA,UAAM,MAAM,KAAK,cAAc,KAAK;AACpC,QAAI,KAAK;AACP,YAAM,IAAI,IAAI,eAAe;AAC7B,YAAM,IAAI,IAAI,gBAAgB;AAC9B,YAAM,SAAS,IAAI,cAAc,EAAE,kBAAkB,GAAG;AACxD,YAAM,UAAU,+BAA+B,KAAK,SAAS,mBAAmB,MAAM,CAAC,CAAC;AACxF,YAAM,MAAM,IAAI,MAAM;AACtB,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,YAAI,SAAS,MAAM,QAAQ;AAC3B,YAAI,UAAU,MAAM,OAAO,IAAI,MAAM,sBAAsB,CAAC;AAC5D,YAAI,MAAM;AAAA,MACZ,CAAC;AACD,YAAM,MAAM,SAAS,cAAc,QAAQ;AAC3C,UAAI,QAAQ;AACZ,UAAI,SAAS;AACb,YAAM,MAAM,IAAI,WAAW,IAAI;AAC/B,UAAI,CAAC,IAAK,OAAM,IAAI,MAAM,wBAAwB;AAClD,UAAI,YAAY;AAChB,UAAI,SAAS,GAAG,GAAG,GAAG,CAAC;AACvB,UAAI,UAAU,KAAK,GAAG,GAAG,GAAG,CAAC;AAC7B,eAAS,KAAK,YAAY,IAAI;AAC9B,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,SAAS,CAAC;AAAA,QACV,UAAU,CAAC;AAAA,QACX,gBAAgB,CAAC;AAAA,QACjB,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE;AAAA,QAC5B,UAAU,CAAC;AAAA,MACb;AAAA,IACF;AAEA,aAAS,KAAK,YAAY,IAAI;AAC9B,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD,SAAS,GAAG;AACV,QAAI,KAAK,WAAY,UAAS,KAAK,YAAY,IAAI;AACnD,UAAM;AAAA,EACR;AACF;AA0CA,IAAM,iBAAsD;AAAA;AAAA;AAAA,EAG1D,aAAa;AAAA,IACX,SAAS;AAAA,IAAK,QAAQ;AAAA,IACtB,IAAI,EAAE,KAAK,KAAK,KAAK,IAAI,MAAM,KAAK,cAAc,KAAK,eAAe,KAAK;AAAA,IAC3E,MAAM,EAAE,WAAW,KAAK,OAAO,KAAK,QAAQ,OAAO,SAAS,KAAK,MAAM,GAAG;AAAA,IAC1E,OAAO;AAAA,IACP,QAAQ,EAAE,OAAO,KAAK,UAAU,MAAM,KAAK,KAAK;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAIA,SAAS;AAAA,IACP,SAAS;AAAA,IAAK,QAAQ;AAAA,IACtB,IAAI,EAAE,KAAK,KAAK,KAAK,GAAG,MAAM,GAAG,cAAc,KAAK,eAAe,IAAK;AAAA,IACxE,MAAM,EAAE,WAAW,KAAK,OAAO,GAAK,QAAQ,MAAO,SAAS,MAAM,MAAM,GAAG;AAAA,IAC3E,OAAO;AAAA,IACP,QAAQ,EAAE,OAAO,KAAK,UAAU,MAAM,KAAK,IAAK;AAAA,EAClD;AAAA;AAAA,EAEA,KAAK;AAAA,IACH,SAAS;AAAA,IAAK,QAAQ;AAAA,IACtB,IAAI,EAAE,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,cAAc,KAAK,eAAe,KAAK;AAAA,IACtE,MAAM,EAAE,WAAW,KAAK,OAAO,GAAK,QAAQ,MAAO,SAAS,MAAM,MAAM,GAAG;AAAA,IAC3E,OAAO;AAAA,IACP,QAAQ,EAAE,OAAO,GAAK,UAAU,GAAG,KAAK,EAAE;AAAA,EAC5C;AACF;AAEA,eAAsB,mBAAmB,MAgBf;AACxB,QAAM,UAAU,eAAe,MAAM,WAAW,aAAa;AAC7D,QAAM,UAAU,MAAM,WAAW,QAAQ;AACzC,QAAM,WAAW,MAAM,YAAY;AACnC,QAAM,YAAY,MAAM,aAAa;AAGrC,QAAM,OAAQ,MAAM,OAAO,MAAM;AACjC,QAAM,KAAK,MAAM;AAIjB,QAAM,UAAU,wBAAwB,MAAM;AAAA,IAC5C,MAAM,MAAM,cAAc,uBAAuB;AAAA,IACjD,SAAS;AAAA,IACT;AAAA,IACA,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,sBAAsB;AAAA,EACxB,CAAC;AAMD,QAAM,KAAK,IAAI,KAAK,IAAI,QAAQ,EAAE;AAClC,QAAM,aAAa,IAAI,KAAK,WAAW,QAAQ,IAAI;AACnD,QAAM,UAAU,IAAI,KAAK,cAAc,QAAQ,KAAK;AACpD,QAAM,UAAU,IAAI,KAAK,QAAQ,IAAI;AAErC,QAAM,SACJ,QAAQ,OAAO,MAAM,IACjB,MAAM;AAAA,IACJ;AAAA,IACA,EAAE,OAAO,QAAQ,OAAO,OAAO,KAAK,QAAQ,OAAO,IAAI;AAAA,IACvD;AAAA,EACF,IACA;AACN,MAAI,OAAQ,QAAO,WAAW,QAAQ,OAAO;AAG7C,QAAM,aAAa,SAAS,CAAC,IAAI,YAAY,SAAS,QAAQ,OAAO,IAAI,CAAC,IAAI,YAAY,SAAS,OAAO;AAC1G,UAAQ,MAAM,GAAG,UAAU;AAE3B,QAAM,MAAM,KAAK,WAAW,EAAE;AAC9B,QAAM,YAAY,IAAI,6BAA6B;AACnD,UAAQ,QAAQ,SAAS;AAEzB,UAAQ,QAAQ,KAAK,eAAe,CAAC;AAIrC,MAAI,eAAoC;AACxC,MAAI,MAAM,UAAU;AAClB,mBAAe,IAAI,eAAe;AAClC,iBAAa,UAAU;AACvB,iBAAa,wBAAwB;AACrC,YAAQ,QAAQ,YAAY;AAAA,EAC9B;AAIA,MAAI,WAAW;AACb,UAAM,SAAS,IAAI,qBAAqB;AACxC,WAAO,OAAO,QAAQ;AACtB,WAAO,QAAQ,SAAS;AACxB,WAAO,MAAM;AAAA,EACf;AAEA,QAAM,KAAK,OAAO;AAElB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAyB;AACvB,aAAO,UAAU;AAAA,IACnB;AAAA,IACA,cAAmC;AACjC,aAAO;AAAA,IACT;AAAA,IACA,WAAmB;AACjB,aAAO,KAAK,IAAI;AAAA,IAClB;AAAA,IACA,OAAa;AACX,UAAI;AACF,QAAC,QAAmD,aAAa;AAAA,MACnE,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;","names":["y","x"]}
|
|
1
|
+
{"version":3,"sources":["../src/promo.ts"],"sourcesContent":["// Promo utilities — browser-only except for parseMidi/midiDurationMs which are\n// pure (no DOM).\n//\n// Exports:\n// parseMidi / midiDurationMs — pure MIDI parser (no DOM; testable in Node)\n// renderNotation — OSMD canvas renderer (browser/OSMD only)\n// createPromoSampler — Tone.js sampler factory (browser/Tone only)\n//\n// Unit tests cover parseMidi (tests/midi.test.ts).\n// renderNotation and createPromoSampler are browser-only — consumers' dom tests\n// cover them (OSMD and Tone.js require a browser context).\n\n// ─── MIDI parser ──────────────────────────────────────────────────────────────\n// Moved verbatim from realmusictheory/site/src/lib/promo/midiDuration.ts.\n\nexport interface MidiNote {\n midi: number;\n startMs: number;\n /** Audible duration in ms — extended while the sustain pedal is held. */\n durMs: number;\n /** 0..1 */\n velocity: number;\n}\n\nexport interface ParsedMidi {\n /** When the last notated note is released (pre-pedal-tail), in ms — paces the playhead. */\n durationMs: number;\n notes: MidiNote[];\n}\n\ninterface RawEvent {\n tick: number;\n kind: 'on' | 'off' | 'sustain' | 'tempo';\n midi?: number;\n velocity?: number;\n on?: boolean; // sustain down?\n us?: number; // tempo in µs/beat\n}\n\nexport function parseMidi(buf: ArrayBuffer): ParsedMidi {\n try {\n const dv = new DataView(buf);\n let p = 0;\n const u8 = () => dv.getUint8(p++);\n const u16 = () => { const v = dv.getUint16(p); p += 2; return v; };\n const u32 = () => { const v = dv.getUint32(p); p += 4; return v; };\n\n if (u32() !== 0x4d546864) return { durationMs: 0, notes: [] }; // 'MThd'\n const headerLen = u32();\n u16(); // format\n const ntrk = u16();\n const division = u16();\n p = 8 + headerLen;\n if (division & 0x8000) return { durationMs: 0, notes: [] }; // SMPTE — not handled\n const tpq = division || 480;\n\n const events: RawEvent[] = [];\n for (let t = 0; t < ntrk; t++) {\n if (p + 8 > dv.byteLength || u32() !== 0x4d54726b) break; // 'MTrk'\n const len = u32();\n const end = Math.min(p + len, dv.byteLength);\n let tick = 0;\n let running = 0;\n while (p < end) {\n let dt = 0, b: number;\n do { b = u8(); dt = (dt << 7) | (b & 0x7f); } while (b & 0x80 && p < end);\n tick += dt;\n let status = dv.getUint8(p);\n if (status & 0x80) { p++; running = status; } else { status = running; }\n if (status === 0xff) {\n const type = u8();\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n if (type === 0x51 && l === 3) {\n events.push({\n tick,\n kind: 'tempo',\n us: (dv.getUint8(p) << 16) | (dv.getUint8(p + 1) << 8) | dv.getUint8(p + 2),\n });\n }\n p += l;\n } else if (status === 0xf0 || status === 0xf7) {\n let l = 0, bb: number;\n do { bb = u8(); l = (l << 7) | (bb & 0x7f); } while (bb & 0x80 && p < end);\n p += l;\n } else {\n const hi = status & 0xf0;\n if (hi === 0x90 || hi === 0x80) {\n const midi = u8();\n const vel = u8();\n if (hi === 0x90 && vel > 0) events.push({ tick, kind: 'on', midi, velocity: vel / 127 });\n else events.push({ tick, kind: 'off', midi });\n } else if (hi === 0xb0) {\n const cc = u8();\n const val = u8();\n if (cc === 64) events.push({ tick, kind: 'sustain', on: val >= 64 });\n } else {\n p += hi === 0xc0 || hi === 0xd0 ? 1 : 2;\n }\n }\n }\n p = end;\n }\n\n // tick → ms via the tempo map\n const tempos = events.filter((e) => e.kind === 'tempo').sort((a, b) => a.tick - b.tick);\n if (!tempos.length || tempos[0].tick > 0) tempos.unshift({ tick: 0, kind: 'tempo', us: 500000 });\n const tickToMs = (tick: number): number => {\n let ms = 0;\n for (let i = 0; i < tempos.length; i++) {\n const segStart = tempos[i].tick;\n if (segStart >= tick) break;\n const segEnd = i + 1 < tempos.length ? Math.min(tempos[i + 1].tick, tick) : tick;\n ms += ((segEnd - segStart) / tpq) * ((tempos[i].us ?? 500000) / 1000);\n }\n return ms;\n };\n\n // Sustain spans (tick ranges where the pedal is down)\n const sustainEvents = events.filter((e) => e.kind === 'sustain').sort((a, b) => a.tick - b.tick);\n const pedalUpAfter = (tick: number): number | null => {\n for (const s of sustainEvents) if (!s.on && s.tick >= tick) return s.tick;\n return null;\n };\n const pedalDownAt = (tick: number): boolean => {\n let down = false;\n for (const s of sustainEvents) { if (s.tick > tick) break; down = !!s.on; }\n return down;\n };\n\n // Pair note-ons with the next matching note-off\n const ordered = events.filter((e) => e.kind === 'on' || e.kind === 'off').sort((a, b) => a.tick - b.tick);\n const open: Record<number, { tick: number; vel: number }[]> = {};\n const notes: MidiNote[] = [];\n let lastOffTick = 0;\n for (const e of ordered) {\n const m = e.midi!;\n if (e.kind === 'on') {\n (open[m] ??= []).push({ tick: e.tick, vel: e.velocity ?? 0.7 });\n } else {\n const stack = open[m];\n if (stack && stack.length) {\n const start = stack.shift()!;\n let endTick = e.tick;\n lastOffTick = Math.max(lastOffTick, endTick);\n // Extend while pedal is held past the note-off.\n if (pedalDownAt(endTick)) {\n const up = pedalUpAfter(endTick);\n if (up != null) endTick = up;\n }\n const startMs = tickToMs(start.tick);\n notes.push({\n midi: m,\n startMs,\n durMs: Math.max(60, tickToMs(endTick) - startMs),\n velocity: start.vel,\n });\n }\n }\n }\n\n return { durationMs: tickToMs(lastOffTick), notes };\n } catch {\n return { durationMs: 0, notes: [] };\n }\n}\n\n/** Total playback duration in ms (0 if unparseable). */\nexport function midiDurationMs(buf: ArrayBuffer): number {\n return parseMidi(buf).durationMs;\n}\n\n// ─── Notation renderer ────────────────────────────────────────────────────────\n// Superset of RSR (stave-web-sightread/src/lib/promo/notation.ts) and RMT\n// (realmusictheory/site/src/lib/promo/notation.ts). Both per-staff measure boxes\n// (RSR) and per-measure column union boxes (RMT) are computed every render.\n//\n// Browser-only: requires document + opensheetmusicdisplay. No unit tests here —\n// consumers' dom tests cover renderNotation.\n\nexport interface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/** One staff's slice of a measure, in canvas px. */\nexport interface StaffMeasureBox {\n index: number; // 0-based measure position within the rendered range\n staff: number; // 0 = top staff (RH/treble), 1 = bottom staff (LH/bass)\n box: Box;\n /** x of the measure's FIRST note (canvas px) — past any clef/key/time signature. */\n noteStartX: number;\n}\n\n/** Per-measure column box: union across all staves for that measure, in canvas px. */\nexport interface MeasureColumnBox extends Box {\n noteStartX: number;\n}\n\nexport interface RenderedNotation {\n canvas: HTMLCanvasElement;\n /** Per-row grand-staff system boxes in canvas px, top-to-bottom. */\n systems: Box[];\n /** Per-(measure, staff) boxes — RSR's geometry. */\n measures: StaffMeasureBox[];\n /** Per-measure column union across staves — RMT's geometry. */\n measureColumns: MeasureColumnBox[];\n /** Tight bounding box of all notation in canvas px (page whitespace cropped). */\n content: Box;\n /**\n * Per-distinct-onset engraved column position (measureIndex + fraction across\n * the note region), in time (left→right) order — one entry per played onset,\n * rests excluded, chords/both-hands collapsed. The scroll-cursor anchors to\n * THESE (the notes' real engraved X) instead of a time-fraction, so the\n * playhead lands exactly on each notehead. Empty/absent when extraction fails\n * or the engraving predates this field (cursor falls back to time/ordinal).\n */\n noteCols?: number[];\n}\n\nexport interface RenderNotationOpts {\n /** OSMD drawFrom/drawUpToMeasureNumber. Applied only when provided. */\n bars?: [number, number];\n /** Background fill colour. Default '#faf7f0' (RSR paper). */\n paper?: string;\n /** Pixel-sum threshold below which a pixel is counted as ink.\n * Default 690 (RSR: paper #faf7f0 sum ≈ 737). RMT uses 620. */\n inkSumThreshold?: number;\n /** Host div width in CSS px. Default 560 (RSR). RMT uses 620. */\n hostWidth?: number;\n /**\n * Notation scroll mode — drives BOTH the engraving layout here and the\n * follow-camera in the scroll-cursor/notation layers.\n *\n * 'hstack' (default) — engrave all measures on ONE horizontal staffline\n * (OSMD RenderSingleHorizontalStaffline). The follow window is a purely\n * HORIZONTAL slice so the playhead pans left→right with no vertical\n * row-break jump. The rasterized bitmap is one very wide row; geometry\n * (systems/measures) all share one row of y.\n *\n * 'vstack' — engrave the classic STACKED systems (normal page wrap into\n * multiple rows). The follow camera frames the ACTIVE system at a fixed\n * band position and scrolls VERTICALLY (eased) to the next system as the\n * playhead crosses systems — never a vertical leap across staff lines.\n *\n * Default 'hstack' (preserves the current single-row behavior).\n */\n scrollMode?: 'hstack' | 'vstack';\n /**\n * @deprecated Use `scrollMode` instead. Back-compat alias: `singleRow:true`\n * ⇔ `scrollMode:'hstack'`, `singleRow:false` ⇔ `scrollMode:'vstack'`.\n * When both are given, `scrollMode` wins.\n */\n singleRow?: boolean;\n}\n\n/** Padded union of boxes, clamped to the canvas. */\nfunction unionBox(boxes: Box[], canvas: HTMLCanvasElement, pad: number): Box {\n if (!boxes.length) return { x: 0, y: 0, w: canvas.width, h: canvas.height };\n const minX = Math.min(...boxes.map((b) => b.x));\n const minY = Math.min(...boxes.map((b) => b.y));\n const maxX = Math.max(...boxes.map((b) => b.x + b.w));\n const maxY = Math.max(...boxes.map((b) => b.y + b.h));\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(canvas.width, maxX + pad) - x,\n h: Math.min(canvas.height, maxY + pad) - y,\n };\n}\n\n/** Tight box around all non-paper pixels (notes, ledger lines, stems), padded. */\nfunction inkBoundingBox(canvas: HTMLCanvasElement, inkSumThreshold: number): Box | null {\n try {\n const ctx = canvas.getContext('2d', { willReadFrequently: true });\n if (!ctx) return null;\n const { width: W, height: H } = canvas;\n const data = ctx.getImageData(0, 0, W, H).data;\n let minX = W, minY = H, maxX = -1, maxY = -1;\n const step = 2;\n for (let y = 0; y < H; y += step) {\n for (let x = 0; x < W; x += step) {\n const i = (y * W + x) * 4;\n if (data[i + 3] < 16) continue;\n if (data[i] + data[i + 1] + data[i + 2] < inkSumThreshold) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n }\n }\n if (maxX < 0) return null;\n const pad = 16;\n const x = Math.max(0, minX - pad);\n const y = Math.max(0, minY - pad);\n return {\n x,\n y,\n w: Math.min(W, maxX + pad) - x,\n h: Math.min(H, maxY + pad) - y,\n };\n } catch {\n return null;\n }\n}\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\nfunction extractGeometry(\n osmd: any,\n canvas: HTMLCanvasElement,\n inkSumThreshold: number,\n): { systems: Box[]; measures: StaffMeasureBox[]; measureColumns: MeasureColumnBox[]; content: Box; noteCols: number[] } {\n const full: Box = { x: 0, y: 0, w: canvas.width, h: canvas.height };\n try {\n const graphic: any = osmd.GraphicSheet;\n const page: any = graphic?.MusicPages?.[0];\n const pageW: number = page?.PositionAndShape?.Size?.width;\n const musicSystems: any[] = page?.MusicSystems ?? [];\n if (!pageW || !musicSystems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n // OSMD units → canvas px\n const f = canvas.width / pageW;\n const toBox = (pas: any): Box | null => {\n const p = pas?.AbsolutePosition;\n const sz = pas?.Size;\n if (!p || !sz) return null;\n return { x: p.x * f, y: p.y * f, w: sz.width * f, h: sz.height * f };\n };\n\n const systems: Box[] = musicSystems\n .map((s) => toBox(s?.PositionAndShape))\n .filter((b): b is Box => !!b && b.w > 1 && b.h > 1)\n .sort((a, b) => a.y - b.y);\n if (!systems.length) return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n\n const measureList: any[][] = graphic?.MeasureList ?? [];\n\n // RSR geometry: per-(measure, staff) individual boxes\n const measures: StaffMeasureBox[] = [];\n measureList.forEach((staves, index) => {\n (staves ?? []).forEach((m: any, staff: number) => {\n const box = toBox(m?.PositionAndShape);\n if (box && box.w > 1 && box.h > 1) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n const noteStartX = typeof seX === 'number' ? seX * f : box.x;\n measures.push({ index, staff, box, noteStartX });\n }\n });\n });\n\n // RMT geometry: per-measure column boxes (union across staves)\n const measureColumns: MeasureColumnBox[] = measureList\n .map((staves) => {\n const arr = staves ?? [];\n const boxes = arr\n .map((m: any) => toBox(m?.PositionAndShape))\n .filter((b: Box | null): b is Box => !!b && b.w > 1 && b.h > 1);\n if (!boxes.length) return null;\n const x0 = Math.min(...boxes.map((b) => b.x));\n const y0 = Math.min(...boxes.map((b) => b.y));\n const x1 = Math.max(...boxes.map((b) => b.x + b.w));\n const y1 = Math.max(...boxes.map((b) => b.y + b.h));\n // First note x across all staves; clamped into the column box\n let nx = Infinity;\n for (const m of arr) {\n const seX = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof seX === 'number') nx = Math.min(nx, seX * f);\n }\n const noteStartX = Number.isFinite(nx) ? Math.max(x0, Math.min(nx, x1)) : x0;\n return { x: x0, y: y0, w: x1 - x0, h: y1 - y0, noteStartX };\n })\n .filter((b): b is MeasureColumnBox => !!b);\n\n // Per-distinct-onset engraved columns, in render (= time) order, rests excluded\n // — the cursor's TRUE anchor X. Each value is `ordinal + frac`, where `ordinal`\n // is the column's position among the DRAWN measures (matching the array index of\n // measureColumnsFromLayout — NOT the absolute MeasureList index, which would\n // overflow the columns array for excerpts whose first measure isn't bar 0), and\n // `frac` is the notehead's position across the measure's note region. Crucially\n // we iterate ONLY drawn measures (a valid box): with drawFrom/drawUpTo, OSMD's\n // MeasureList still holds EVERY source measure, so without this filter the whole\n // score leaks in (e.g. 128 cols for a 22-note excerpt) and never pairs 1:1 with\n // the played onsets, disabling the engraved-x anchor.\n // We key each note's engraved X by its rhythmic TIMESTAMP within the measure,\n // NOT by raw distinct X. Two staves' notes at the SAME beat engrave at slightly\n // different X (the grand staff isn't pixel-aligned), so deduping by X yielded\n // MORE columns than there are onsets (e.g. 45 cols vs 40 onsets) — which broke\n // the 1:1 pairing with the played onsets and forced the cursor onto a\n // linear-time fallback that drifts ~½ bar ahead of the real noteheads on dense\n // bars. Keying by timestamp collapses each beat to ONE column at the (leftmost)\n // engraved X of its noteheads, so the column count equals the distinct-onset\n // count and the cursor lands on the actual notehead, beat for beat.\n const noteCols: number[] = [];\n let ordinal = 0;\n measureList.forEach((staves) => {\n const arr = staves ?? [];\n let mx0 = Infinity, mx1 = -Infinity, nsx = Infinity;\n const byTs = new Map<number, number>(); // rhythmic timestamp → leftmost note X (px)\n for (const m of arr) {\n const mb = toBox(m?.PositionAndShape);\n if (mb) { mx0 = Math.min(mx0, mb.x); mx1 = Math.max(mx1, mb.x + mb.w); }\n const se0 = (m?.staffEntries ?? [])[0]?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof se0 === 'number') nsx = Math.min(nsx, se0 * f);\n for (const se of (m?.staffEntries ?? [])) {\n const sx = se?.PositionAndShape?.AbsolutePosition?.x;\n if (typeof sx !== 'number') continue;\n const hasNote = (se?.graphicalVoiceEntries ?? []).some(\n (gve: any) => (gve?.notes ?? []).some(\n (gn: any) => !(gn?.sourceNote?.isRestFlag ?? gn?.sourceNote?.IsRest ?? false)));\n if (!hasNote) continue;\n // Within-measure rhythmic position (whole notes). Round to a fine grid so\n // float noise doesn't split a beat; falls back to X if unavailable.\n const tsRaw =\n se?.relInMeasureTimestamp?.RealValue ??\n se?.getAbsoluteTimestamp?.()?.RealValue ??\n se?.sourceStaffEntry?.Timestamp?.RealValue;\n const key = typeof tsRaw === 'number' ? Math.round(tsRaw * 10000) : Math.round(sx * f);\n const px = sx * f;\n const prev = byTs.get(key);\n if (prev === undefined || px < prev) byTs.set(key, px);\n }\n }\n // Skip undrawn measures (no valid box / zero width) — only drawn measures get\n // a column, so `ordinal` stays in lockstep with measureColumnsFromLayout.\n if (!Number.isFinite(mx0) || (mx1 - mx0) <= 1) return;\n const startX = Number.isFinite(nsx) ? Math.max(mx0, Math.min(nsx, mx1)) : mx0;\n const denom = (mx1 - startX) || 1;\n // Emit one column per distinct beat, in time order (the byTs keys are the\n // rounded timestamps), so column k ↔ onset k.\n for (const key of [...byTs.keys()].sort((a, b) => a - b)) {\n const x = byTs.get(key)!;\n noteCols.push(ordinal + Math.min(1, Math.max(0, (x - startX) / denom)));\n }\n ordinal++;\n });\n\n const content = inkBoundingBox(canvas, inkSumThreshold) ?? unionBox(systems, canvas, 14);\n return { systems, measures, measureColumns, content, noteCols };\n } catch {\n return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };\n }\n}\n/* eslint-enable @typescript-eslint/no-explicit-any */\n\n/** Render a MusicXML string to a detached canvas + geometry.\n * Browser-only (requires document + opensheetmusicdisplay dynamic import). */\nexport async function renderNotation(\n xml: string,\n opts?: RenderNotationOpts,\n): Promise<RenderedNotation> {\n const paper = opts?.paper ?? '#faf7f0';\n const inkSumThreshold = opts?.inkSumThreshold ?? 690;\n const hostWidth = opts?.hostWidth ?? 560;\n // scrollMode drives the engraving layout; singleRow is the deprecated alias.\n // Default 'hstack' (single horizontal staffline) preserves current behavior.\n const scrollMode: 'hstack' | 'vstack' =\n opts?.scrollMode ?? (opts?.singleRow === false ? 'vstack' : 'hstack');\n const singleRow = scrollMode === 'hstack';\n\n // Literal dynamic import: consumers' bundlers (Vite/Rollup) must be able to\n // statically see the specifier to resolve + code-split it — a variable\n // specifier would reach the browser as a bare import and fail at runtime.\n const { OpenSheetMusicDisplay } = await import('opensheetmusicdisplay');\n\n const host = document.createElement('div');\n host.style.cssText = `position:fixed;left:-9999px;top:0;width:${hostWidth}px;background:${paper};`;\n document.body.appendChild(host);\n\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const osmd: any = new OpenSheetMusicDisplay(host, {\n backend: 'canvas',\n autoResize: false,\n drawTitle: false,\n drawSubtitle: false,\n drawComposer: false,\n drawLyricist: false,\n drawPartNames: false,\n });\n\n await osmd.load(xml);\n\n // hstack: engrave on a single horizontal staffline so the follow window pans\n // purely HORIZONTALLY (no stacked-system row breaks → no vertical playhead\n // jump). vstack: leave OSMD's default page wrap → classic stacked systems\n // (the follow camera then scrolls vertically between systems). Must be set\n // before render(); when on, the rasterized canvas becomes one wide row.\n if (singleRow) {\n osmd.setOptions({ renderSingleHorizontalStaffline: true } as never);\n }\n\n // Apply bar range only when provided (RSR's drawFrom/drawUpTo).\n if (opts?.bars) {\n osmd.setOptions({\n drawFromMeasureNumber: opts.bars[0],\n drawUpToMeasureNumber: opts.bars[1],\n } as never);\n }\n\n osmd.render();\n\n const canvas = host.querySelector('canvas');\n if (canvas) {\n const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);\n document.body.removeChild(host);\n return { canvas, systems, measures, measureColumns, content, noteCols };\n }\n\n // SVG fallback: rasterize into a canvas so callers always get a canvas.\n const svg = host.querySelector('svg');\n if (svg) {\n const w = svg.clientWidth || hostWidth;\n const h = svg.clientHeight || 300;\n const svgStr = new XMLSerializer().serializeToString(svg);\n const dataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr)));\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error('svg rasterize failed'));\n img.src = dataUrl;\n });\n const out = document.createElement('canvas');\n out.width = w;\n out.height = h;\n const c2d = out.getContext('2d');\n if (!c2d) throw new Error('2d context unavailable');\n c2d.fillStyle = paper;\n c2d.fillRect(0, 0, w, h);\n c2d.drawImage(img, 0, 0, w, h);\n document.body.removeChild(host);\n return {\n canvas: out,\n systems: [],\n measures: [],\n measureColumns: [],\n content: { x: 0, y: 0, w, h },\n noteCols: [],\n };\n }\n\n document.body.removeChild(host);\n throw new Error('OSMD produced neither canvas nor SVG');\n } catch (e) {\n if (host.parentNode) document.body.removeChild(host);\n throw e;\n }\n}\n\n// ─── Promo sampler ────────────────────────────────────────────────────────────\n// The shared Tone.js setup prefix under both apps' createPromoAudio.\n// Scheduling (playEvents / playMidi / playWindowSolo / playForeground) stays\n// in each app.\n//\n// Browser-only: requires Tone.js dynamic import. No unit tests here —\n// consumers' dom tests cover createPromoSampler.\n\nimport { createSalamanderSampler, generateReverb } from './audioHelpers';\nimport { SALAMANDER_CDN_BASE, SALAMANDER_URLS_8, SALAMANDER_URLS_FULL } from './salamander';\n\nexport interface PromoSampler {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n Tone: any; // typeof Tone — typed loose like audioHelpers.ts does for Tone\n sampler: unknown; // Tone.Sampler; typed loose like audioHelpers\n getStream(): MediaStream;\n /** Master-bus analyser (post-chain, sink-only) for reactive visualizers, or\n * null if `analyser` wasn't requested. whozart's \"listen\" spectrum reads it. */\n getAnalyser(): AnalyserNode | null;\n /** Current audio-clock time in seconds (Tone.now()). */\n audioNow(): number;\n /** releaseAll on the sampler; safe no-op on failure. */\n stop(): void;\n}\n\n/** Per-app tone shaping for the shared promo mastering bus. Ported from\n * whozart's audio-impl chain (the richest of the fleet); `reading` is a drier,\n * closer voicing so sight-reading notation stays legible note-to-note. Apps\n * pick one via `createPromoSampler({ voicing })`. */\nexport type PromoVoicing = 'concertHall' | 'reading' | 'dry';\n\ninterface VoicingPreset {\n release: number;\n attack: number;\n eq: { low: number; mid: number; high: number; lowFrequency: number; highFrequency: number };\n comp: { threshold: number; ratio: number; attack: number; release: number; knee: number };\n widen: number;\n reverb: { decay: number; preDelay: number; wet: number };\n}\n\nconst PROMO_VOICINGS: Record<PromoVoicing, VoicingPreset> = {\n // whozart's hall sound: long ringing release, lifted lows/air, gentle glue\n // compression, wide stereo, a real-hall reverb tail.\n concertHall: {\n release: 1.8, attack: 0.005,\n eq: { low: 1.5, mid: -1, high: 1.5, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -18, ratio: 1.6, attack: 0.012, release: 0.3, knee: 18 },\n widen: 0.30,\n reverb: { decay: 3.5, preDelay: 0.04, wet: 0.22 },\n },\n // Sight-reading: shorter release + much drier/shorter reverb so consecutive\n // notes don't smear into each other while the eye tracks the staff. Small\n // presence bump in the mids for note-attack clarity, narrower stereo.\n reading: {\n release: 1.1, attack: 0.004,\n eq: { low: 0.5, mid: 1, high: 1, lowFrequency: 280, highFrequency: 5000 },\n comp: { threshold: -16, ratio: 2.0, attack: 0.008, release: 0.25, knee: 12 },\n widen: 0.18,\n reverb: { decay: 1.4, preDelay: 0.02, wet: 0.10 },\n },\n // Bone-dry — no reverb at all (e.g. a debugging/reference voicing).\n dry: {\n release: 1.0, attack: 0.004,\n eq: { low: 0, mid: 0, high: 0, lowFrequency: 280, highFrequency: 4500 },\n comp: { threshold: -14, ratio: 2.0, attack: 0.008, release: 0.25, knee: 10 },\n widen: 0,\n reverb: { decay: 1.0, preDelay: 0, wet: 0 },\n },\n};\n\nexport async function createPromoSampler(opts?: {\n /** Feed a 0-value ConstantSource into the capture stream so the recorder's\n * audio track is live from t=0 (silent intro scenes aren't dropped).\n * Default false (RSR behavior). RMT passes true. */\n keepAlive?: boolean;\n /** Override the voicing's sampler release time (seconds). */\n release?: number;\n /** Sampler volume in dB. Default -2. */\n volumeDb?: number;\n /** Per-app tone shaping (see PromoVoicing). Default 'concertHall'. */\n voicing?: PromoVoicing;\n /** Use the full A0..C8 Salamander anchor set instead of the 8-anchor set —\n * fuller tone, more samples to fetch. Default false (8-anchor). */\n fullSamples?: boolean;\n /** Expose a master-bus analyser (for reactive visualizers). Default false. */\n analyser?: boolean;\n}): Promise<PromoSampler> {\n const voicing = PROMO_VOICINGS[opts?.voicing ?? 'concertHall'];\n const release = opts?.release ?? voicing.release;\n const volumeDb = opts?.volumeDb ?? -2;\n const keepAlive = opts?.keepAlive ?? false;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const Tone = (await import('tone')) as any;\n await Tone.start();\n\n // Unrouted sampler — we build the mastering bus off it (don't send the dry\n // sampler to the speakers, which would bypass the chain).\n const sampler = createSalamanderSampler(Tone, {\n urls: opts?.fullSamples ? SALAMANDER_URLS_FULL : SALAMANDER_URLS_8,\n baseUrl: SALAMANDER_CDN_BASE,\n release,\n attack: voicing.attack,\n volumeDb,\n connectToDestination: false,\n });\n\n // Mastering bus (ported from whozart): EQ tilt → glue compressor → stereo\n // widener → reverb → brick-wall limiter. Reverb is generated with a timeout\n // fallback so a slow/again-failing IR can never hang the render; on failure\n // the chain simply omits reverb.\n const eq = new Tone.EQ3(voicing.eq);\n const compressor = new Tone.Compressor(voicing.comp);\n const widener = new Tone.StereoWidener(voicing.widen);\n const limiter = new Tone.Limiter(-1.5);\n\n const reverb =\n voicing.reverb.wet > 0\n ? await generateReverb(\n Tone,\n { decay: voicing.reverb.decay, wet: voicing.reverb.wet },\n 4000,\n )\n : null;\n if (reverb) reverb.preDelay = voicing.reverb.preDelay;\n\n // chain() wires node→node in order; tail node feeds the taps below.\n const chainNodes = reverb ? [eq, compressor, widener, reverb, limiter] : [eq, compressor, widener, limiter];\n sampler.chain(...chainNodes);\n\n const ctx = Tone.getContext().rawContext as AudioContext;\n const mediaDest = ctx.createMediaStreamDestination();\n limiter.connect(mediaDest);\n // Also monitor through the speakers so a non-headless preview is audible.\n limiter.connect(Tone.getDestination());\n\n // Optional master-bus analyser (sink-only — no onward connection so it can't\n // double the signal). Drives whozart's reactive \"listen\" visualizer.\n let analyserNode: AnalyserNode | null = null;\n if (opts?.analyser) {\n analyserNode = ctx.createAnalyser();\n analyserNode.fftSize = 2048;\n analyserNode.smoothingTimeConstant = 0.8;\n limiter.connect(analyserNode);\n }\n\n // Optional: keep the audio track alive from t=0 so the recorder doesn't drop\n // silent intro scenes. RMT uses this; RSR does not.\n if (keepAlive) {\n const source = ctx.createConstantSource();\n source.offset.value = 0;\n source.connect(mediaDest);\n source.start();\n }\n\n await Tone.loaded();\n\n return {\n Tone,\n sampler,\n getStream(): MediaStream {\n return mediaDest.stream;\n },\n getAnalyser(): AnalyserNode | null {\n return analyserNode;\n },\n audioNow(): number {\n return Tone.now();\n },\n stop(): void {\n try {\n (sampler as unknown as { releaseAll?: () => void }).releaseAll?.();\n } catch {\n /* no-op */\n }\n },\n };\n}\n"],"mappings":"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","names":["y","x"]}
|
package/dist/scene/index.js
CHANGED
|
@@ -875,7 +875,39 @@ function scrollCursorLayer() {
|
|
|
875
875
|
return (lo + segFrac) / (n - 1);
|
|
876
876
|
}
|
|
877
877
|
function noteColsFor(ctx) {
|
|
878
|
-
|
|
878
|
+
const eng = getNotationEngraving(ctx)?.rendered?.noteCols;
|
|
879
|
+
const onsets = onsetsFor(ctx);
|
|
880
|
+
const n = onsets.length;
|
|
881
|
+
if (!eng || !eng.length) return propNoteCols;
|
|
882
|
+
if (eng.length === n) return eng;
|
|
883
|
+
if (!propNoteCols || propNoteCols.length !== n) return eng;
|
|
884
|
+
const measureOf = (c) => Math.floor(c + 1e-6);
|
|
885
|
+
const engByMeasure = /* @__PURE__ */ new Map();
|
|
886
|
+
for (const e of eng) {
|
|
887
|
+
const m = measureOf(e);
|
|
888
|
+
const a = engByMeasure.get(m);
|
|
889
|
+
if (a) a.push(e);
|
|
890
|
+
else engByMeasure.set(m, [e]);
|
|
891
|
+
}
|
|
892
|
+
for (const a of engByMeasure.values()) a.sort((x, y) => x - y);
|
|
893
|
+
const idxByMeasure = /* @__PURE__ */ new Map();
|
|
894
|
+
propNoteCols.forEach((c, i) => {
|
|
895
|
+
const m = measureOf(c);
|
|
896
|
+
const a = idxByMeasure.get(m);
|
|
897
|
+
if (a) a.push(i);
|
|
898
|
+
else idxByMeasure.set(m, [i]);
|
|
899
|
+
});
|
|
900
|
+
const out = propNoteCols.slice();
|
|
901
|
+
for (const [m, idxs] of idxByMeasure) {
|
|
902
|
+
const e = engByMeasure.get(m);
|
|
903
|
+
if (!e || !e.length) continue;
|
|
904
|
+
const K = idxs.length;
|
|
905
|
+
idxs.forEach((origIdx, i) => {
|
|
906
|
+
const j = K > 1 ? Math.round(i * (e.length - 1) / (K - 1)) : 0;
|
|
907
|
+
out[origIdx] = e[Math.min(j, e.length - 1)];
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return out;
|
|
879
911
|
}
|
|
880
912
|
function followProgress(ctx, tMs) {
|
|
881
913
|
const eng = getNotationEngraving(ctx);
|