@real-music-packages/web-core 0.35.0 → 0.36.1

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.
@@ -0,0 +1,186 @@
1
+ import {
2
+ getNotationEngraving,
3
+ measureColumnsFromLayout,
4
+ notationFactory,
5
+ scrollCursorFactory
6
+ } from "./chunk-JVSXE4X3.js";
7
+ import {
8
+ safeBox
9
+ } from "./chunk-HXTRNE74.js";
10
+
11
+ // src/notationPlayer.ts
12
+ var DEFAULT_THEME = {
13
+ paper: "#faf7f0",
14
+ ink: "#1a1614",
15
+ accent: "#7b2436",
16
+ sepia: "#6d5d4d",
17
+ gold: "#c8a55b",
18
+ fontDisplay: "Georgia, serif",
19
+ fontBody: "Georgia, serif",
20
+ brand: ""
21
+ };
22
+ function scoreFromOnsets(onsetsMs) {
23
+ const notes = onsetsMs.map((onsetMs) => ({
24
+ pitchMidi: 60,
25
+ step: "C",
26
+ alter: 0,
27
+ octave: 4,
28
+ onsetMs,
29
+ durMs: 0,
30
+ staff: 0,
31
+ voice: 0,
32
+ hand: "R",
33
+ measure: 1
34
+ }));
35
+ const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;
36
+ return {
37
+ notes,
38
+ tempoMap: { source: "fallback", segments: [{ atMs: 0, bpm: 120 }] },
39
+ durationMs
40
+ };
41
+ }
42
+ function hitTestMeasureAt(layout, mx, my) {
43
+ if (!layout.measures.length) return null;
44
+ const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);
45
+ const cols = measureColumnsFromLayout(layout.measures);
46
+ let nearest = null;
47
+ let nearestD = Infinity;
48
+ for (let i = 0; i < cols.length; i++) {
49
+ const b = cols[i];
50
+ if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];
51
+ const cx = b.x + b.w / 2;
52
+ const cy = b.y + b.h / 2;
53
+ const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);
54
+ if (d < nearestD) {
55
+ nearestD = d;
56
+ nearest = indices[i];
57
+ }
58
+ }
59
+ return nearest;
60
+ }
61
+ function createNotationPlayer(opts) {
62
+ const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;
63
+ const mode = opts.mode ?? "vstack";
64
+ const theme = { ...DEFAULT_THEME, ...opts.theme };
65
+ const canvas = document.createElement("canvas");
66
+ canvas.style.display = "block";
67
+ canvas.style.width = "100%";
68
+ canvas.style.height = "100%";
69
+ host.appendChild(canvas);
70
+ const dpr = typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
71
+ function frameSize() {
72
+ if (opts.size) return opts.size;
73
+ const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));
74
+ const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));
75
+ return [w, h];
76
+ }
77
+ const [initW, initH] = frameSize();
78
+ canvas.width = initW;
79
+ canvas.height = initH;
80
+ const ctx2d = canvas.getContext("2d");
81
+ if (!ctx2d) throw new Error("createNotationPlayer: 2D canvas context unavailable");
82
+ let lastTMs = 0;
83
+ const audioClock = { nowMs: () => lastTMs };
84
+ const rctx = {
85
+ ctx2d,
86
+ W: initW,
87
+ H: initH,
88
+ score: scoreFromOnsets(onsetsMs),
89
+ audioClock,
90
+ theme,
91
+ safeBox: safeBox(initW, initH),
92
+ fps: 30
93
+ };
94
+ const notation = notationFactory.create();
95
+ const scrollCursor = scrollCursorFactory.create();
96
+ function notationProps(rendered) {
97
+ return {
98
+ ...rendered ? { rendered } : { xml: musicXml },
99
+ scrollMode: mode,
100
+ bars,
101
+ bandTop: opts.bandTop ?? 0,
102
+ bandHeight: opts.bandHeight ?? rctx.H
103
+ };
104
+ }
105
+ const scrollCursorProps = { scrollMode: mode, barDurMs, noteCols };
106
+ let renderedRn = null;
107
+ let destroyed = false;
108
+ const ready = (async () => {
109
+ await notation.init(rctx, notationProps(opts.rendered));
110
+ renderedRn = getNotationEngraving(rctx)?.rendered ?? null;
111
+ await scrollCursor.init(rctx, scrollCursorProps);
112
+ })();
113
+ function clear() {
114
+ ctx2d.save();
115
+ ctx2d.setTransform(1, 0, 0, 1, 0, 0);
116
+ ctx2d.fillStyle = theme.paper;
117
+ ctx2d.fillRect(0, 0, canvas.width, canvas.height);
118
+ ctx2d.restore();
119
+ }
120
+ function setTime(tMs) {
121
+ if (destroyed) return;
122
+ lastTMs = tMs;
123
+ clear();
124
+ notation.draw(rctx, tMs);
125
+ scrollCursor.draw(rctx, tMs);
126
+ }
127
+ async function performResize() {
128
+ const [w, h] = frameSize();
129
+ if (w === canvas.width && h === canvas.height) return;
130
+ canvas.width = w;
131
+ canvas.height = h;
132
+ rctx.W = w;
133
+ rctx.H = h;
134
+ rctx.safeBox = safeBox(w, h);
135
+ if (renderedRn) {
136
+ await notation.init(rctx, notationProps(renderedRn));
137
+ await scrollCursor.init(rctx, scrollCursorProps);
138
+ }
139
+ if (!destroyed) setTime(lastTMs);
140
+ }
141
+ function currentLayout() {
142
+ const eng = getNotationEngraving(rctx);
143
+ if (!eng) return null;
144
+ return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;
145
+ }
146
+ const clickListeners = [];
147
+ function onCanvasClick(e) {
148
+ const rect = canvas.getBoundingClientRect();
149
+ if (!(rect.width > 0) || !(rect.height > 0)) return;
150
+ const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
151
+ const my = (e.clientY - rect.top) * (canvas.height / rect.height);
152
+ const layout = currentLayout();
153
+ if (!layout) return;
154
+ const idx = hitTestMeasureAt(layout, mx, my);
155
+ if (idx != null) for (const cb of clickListeners) cb(idx);
156
+ }
157
+ canvas.addEventListener("click", onCanvasClick);
158
+ return {
159
+ ready,
160
+ setTime,
161
+ resize() {
162
+ void performResize();
163
+ },
164
+ onMeasureClick(cb) {
165
+ clickListeners.push(cb);
166
+ return () => {
167
+ const i = clickListeners.indexOf(cb);
168
+ if (i >= 0) clickListeners.splice(i, 1);
169
+ };
170
+ },
171
+ destroy() {
172
+ if (destroyed) return;
173
+ destroyed = true;
174
+ canvas.removeEventListener("click", onCanvasClick);
175
+ notation.dispose?.();
176
+ scrollCursor.dispose?.();
177
+ clickListeners.length = 0;
178
+ if (canvas.parentNode === host) host.removeChild(canvas);
179
+ }
180
+ };
181
+ }
182
+ export {
183
+ createNotationPlayer,
184
+ hitTestMeasureAt
185
+ };
186
+ //# sourceMappingURL=notationPlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notationPlayer.ts"],"sourcesContent":["// createNotationPlayer — THE canonical notation-playback component: one\n// scrolling-notation-plus-gliding-playhead implementation, used for BOTH live\n// in-app players (this module's reason to exist) and the promo/video-recording\n// path (src/scene/layers/notation.ts + scrollCursor.ts, driven by the SceneSpec\n// runner for a fixed-duration capture). Both consume the SAME geometry —\n// `notationLayout`, `followBoxAt`/`vstackFollowBox`, `audioPlayheadLine`/\n// `vstackAudioPlayheadLine`, `measureColumnsFromLayout` — which still lives in\n// `src/scene/` (notationGeometry.ts, engravingStore.ts, layers/notation.ts,\n// layers/scrollCursor.ts) for now; this module is their canonical PUBLIC\n// surface for interactive, caller-driven playback. It does not reimplement or\n// fork any of that math — it just instantiates the two `Layer`s directly\n// (bypassing the SceneSpec/timeline runner, which is built for a fixed-length\n// recorded clip, not a live `setTime()`-driven widget) and adds the two things\n// a live player needs that a recorded promo never did: resize, and\n// measure-click hit-testing.\n//\n// WHY THIS EXISTS: a prior in-app player (stave-web-sightread's /bach/play)\n// hand-rolled its own playhead interpolation directly against OSMD's graphical\n// output instead of reusing this path, and the result was a janky cursor.\n// Hand-rolling notation/playhead geometry against OSMD is a KNOWN FAILURE MODE\n// — OSMD's rasterized bitmap does not scale by the naive `canvas.width / pageW`\n// whenever the engraving overflows the nominal page width (which the `hstack`\n// single-staffline layout does routinely), so a from-scratch cursor drifts\n// steadily off the noteheads over a piece. That exact bug was found and fixed\n// ONCE, here, in `extractGeometry` (src/promo.ts — `canvas.width /\n// (contentRight + contentLeft)`, not `/pageW`); every consumer of THIS module\n// inherits the fix for free. Don't re-derive playhead/scroll geometry against\n// OSMD anywhere else — wrap this component instead.\n//\n// Follow-up (noted, not done here): src/scene/layers/notation.ts +\n// scrollCursor.ts could themselves be rebuilt on top of this module instead of\n// duplicating the init/draw wiring; deferred to keep this change additive.\n//\n// IMPORT PATH: this module is SUBPATH-ONLY —\n// `import { createNotationPlayer } from '@real-music-packages/web-core/notationPlayer'`\n// — matching every other non-trivial module in this package (promo.ts,\n// video.ts, scene/, server.ts, streak.ts, playback.ts). It is intentionally\n// NOT re-exported from the root barrel (src/index.ts): the root barrel is\n// theory-only (notes/frequency/scales/intervals/chords/fretboard/shareCard,\n// all zero-dependency) so apps that only want pitch/theory helpers don't\n// statically pull in this module's scene/video runtime deps.\n\nimport { notationFactory, type NotationProps } from './scene/layers/notation';\nimport { scrollCursorFactory, type ScrollCursorProps } from './scene/layers/scrollCursor';\nimport { getNotationEngraving } from './scene/engravingStore';\nimport { measureColumnsFromLayout, type NotationLayout } from './scene/notationGeometry';\nimport type { AudioClock, RenderCtx } from './scene/layer';\nimport type { Score, ScoreNote } from './scene/score';\nimport type { RenderedNotation } from './promo';\nimport { safeBox, type PromoTheme } from './video';\n\n/**\n * Notation scroll/camera mode — forwarded verbatim to the underlying engraving\n * + follow-camera (see `RenderNotationOpts.scrollMode` in src/promo.ts and\n * `notationGeometry.ts`'s hstack/vstack follow functions). These are the ONLY\n * two modes the shared machinery implements; there is no third.\n *\n * 'vstack' (DEFAULT here) — classic stacked systems (page-wrap engraving).\n * The follow camera frames one system at a time and scrolls vertically\n * (with a \"carriage return\" playhead) as the piece crosses system breaks.\n * This is the actively-exercised path (whozart's live player, the RSR/RET/\n * RMT promo videos) and the one the memory note \"use vstack everywhere\"\n * refers to — pick it unless you have a specific reason not to.\n *\n * 'hstack' — single horizontal staffline; the follow window pans purely\n * left→right. The cursor is onset-locked here too (audioPlayheadLine, not\n * a free-running sweep — that linear-sweep footgun was removed from the\n * shared cursor entirely in web-core 0.22.0), and the earlier\n * `canvas.width/pageW` scale bug that made hstack drift is fixed (0.30+\n * `extractGeometry`, see the module doc above). It is still the LESS\n * exercised of the two modes for live players (most real players show\n * multi-line music, which needs vstack) — supported and correct today, but\n * treat it as the less battle-tested choice.\n */\nexport type NotationPlayerMode = 'hstack' | 'vstack';\n\nexport interface NotationPlayerTheme extends Partial<PromoTheme> {}\n\nexport interface CreateNotationPlayerOpts {\n /** Element the player's canvas is mounted into (fills it). */\n host: HTMLElement;\n /** MusicXML to engrave. */\n musicXml: string;\n /** Distinct note onsets (ms, sorted or not — de-duped/sorted internally via\n * the shared score model) the playhead locks to. One per played note (or\n * the caller's own onset schedule) — this IS the audio clock's timing. */\n onsetsMs: number[];\n /**\n * Per-onset engraved column positions (`measureIndex + frac`), 1:1 with\n * `onsetsMs`. Optional — falls back to the shared machinery's own\n * time/ordinal-based placement when omitted (see `scroll-cursor`'s\n * `noteCols`). Strongly recommended for anything beyond a demo: it is what\n * lands the cursor on the actual notehead instead of a time-derived guess.\n *\n * GOTCHA (pre-existing in the wrapped machinery, not introduced here):\n * internally the cursor works off `distinctOnsets(onsetsMs)` (deduped +\n * sorted), so if `onsetsMs` contains duplicate onsets (simultaneous chord\n * notes sharing one onset time — a legitimate input), the deduped length no\n * longer matches `noteCols.length` and the `1:1` alignment silently breaks,\n * degrading to the `barDurMs`/ordinal fallback with no warning. Build\n * `noteCols` against the DEDUPED, sorted onset list, not the raw one.\n */\n noteCols?: number[];\n /** Scroll/camera mode. Default 'vstack'. See `NotationPlayerMode`. Fixed for\n * the life of the instance — switching modes needs a fresh OSMD engrave\n * (different `RenderSingleHorizontalStaffline` layout), so it is NOT a\n * runtime-switchable option; create a new player if the mode must change\n * (noted as a follow-up, not built: nothing in the shared machinery makes a\n * live re-layout trivial). */\n mode?: NotationPlayerMode;\n /** Duration of one measure in ms — enables time-accurate cursor/window\n * placement when `noteCols` isn't supplied (see `scroll-cursor`'s\n * `barDurMs`). */\n barDurMs?: number;\n /** Bar range [from,to] forwarded to the engraver (renderNotation\n * bars/drawFrom-drawUpTo) — engrave an excerpt rather than the whole score. */\n bars?: [number, number];\n /** Top of the notation band, screen px. Default 0 (fills the host — this is\n * a UI widget, not a phone-safe video frame). */\n bandTop?: number;\n /** Height of the notation band, screen px. Default the full frame height. */\n bandHeight?: number;\n /** Theme tokens (colours/fonts) forwarded to the layers. Any field omitted\n * falls back to a neutral default. */\n theme?: NotationPlayerTheme;\n /** Explicit canvas size in device px. Default: host.clientWidth/clientHeight\n * × devicePixelRatio. Pass this in test/headless environments where the\n * host has no real layout (e.g. jsdom, where clientWidth is always 0). */\n size?: [number, number];\n /**\n * Advanced / test seam: a pre-rasterized engraving, bypassing the browser\n * OSMD raster (`musicXml` is still required by the type but is ignored when\n * this is set). Mirrors `NotationProps.rendered` in\n * `src/scene/layers/notation.ts` (\"test/headless injection\") — real OSMD\n * *rendering* (as opposed to parsing) needs a real 2D canvas context (glyph\n * metrics for its line-breaking pass) that headless/jsdom test contexts\n * can't fully provide, so this is how this module (and the layer it wraps)\n * stays unit-testable in Node. Not needed in a real browser host.\n */\n rendered?: RenderedNotation;\n}\n\nexport interface NotationPlayer {\n /** Resolves once the engraving has been rasterized and is ready to draw.\n * `setTime`/click hit-testing are safe to call before this resolves — they\n * simply no-op (draw a blank frame / report no measure) until ready. */\n readonly ready: Promise<void>;\n /**\n * Draw the frame for absolute playback time `tMs`. The caller owns the\n * audio clock + the rAF loop — call this every frame with the current\n * playback position; everything else (follow camera, onset-locked\n * playhead) is a pure function of `tMs`, exactly as the promo/whozart path\n * drives it.\n */\n setTime(tMs: number): void;\n /** Re-measure the host and resize the canvas to match (device-px aware).\n * Cheap after the first draw — reuses the already-rasterized engraving\n * bitmap, it does not re-run OSMD. Call on host resize / orientation\n * change. Fire-and-forget (async internally; the next `setTime` reflects\n * the new size once it lands, typically within a microtask). */\n resize(): void;\n /** Register a measure-click handler: fires with the clicked measure's\n * engraved index (matching `ScoreNote.measure` numbering) when a click\n * lands on — or nearest to — a rendered measure column. Multiple handlers\n * may be registered (they all fire); returns an unsubscribe function for\n * that one handler. `destroy()` also clears every remaining listener. */\n onMeasureClick(cb: (measureIndex: number) => void): () => void;\n /** Tear down: removes the canvas from `host` and drops listeners/state. */\n destroy(): void;\n}\n\nconst DEFAULT_THEME: PromoTheme = {\n paper: '#faf7f0', ink: '#1a1614', accent: '#7b2436', sepia: '#6d5d4d', gold: '#c8a55b',\n fontDisplay: 'Georgia, serif', fontBody: 'Georgia, serif', brand: '',\n};\n\n/** A minimal but valid `Score` carrying only the onset timings the shared\n * scroll-cursor needs (`distinctOnsets(ctx.score.notes)`). Pitch/hand/measure\n * fields are placeholders — the notation + scroll-cursor layers never read\n * them (they read the RASTERIZED engraving's own geometry for position; the\n * Score here only supplies the audio-onset clock). */\nfunction scoreFromOnsets(onsetsMs: number[]): Score {\n const notes: ScoreNote[] = onsetsMs.map((onsetMs) => ({\n pitchMidi: 60, step: 'C', alter: 0, octave: 4,\n onsetMs, durMs: 0, staff: 0, voice: 0, hand: 'R', measure: 1,\n }));\n const durationMs = onsetsMs.length ? Math.max(...onsetsMs) : 0;\n return {\n notes,\n tempoMap: { source: 'fallback', segments: [{ atMs: 0, bpm: 120 }] },\n durationMs,\n };\n}\n\n/**\n * Pure hit-test: which measure (by its engraved index) contains — or is\n * nearest to — point (mx, my) in a given followed layout. Reuses\n * `measureColumnsFromLayout` (the SAME per-measure union boxes the playhead\n * anchors to) for the boxes; only the index bookkeeping (matching each\n * returned box back to its measure index, which `measureColumnsFromLayout`\n * intentionally drops — the playhead has no use for it) is new here, and it\n * is pure array/index bookkeeping, not geometry math. Exported standalone so\n * it is unit-testable without a DOM/canvas.\n *\n * COORDINATE SPACE — read this before calling directly: (mx, my) MUST be in\n * the same DEST/DEVICE-PIXEL space `layout.measures[].box` is already mapped\n * into by `notationLayout()` (i.e. the player's own `<canvas>` device pixels,\n * origin top-left, NOT CSS px) — the same space this module's own\n * `onCanvasClick` computes via `(clientX - rect.left) * (canvas.width /\n * rect.width)`. It is NOT the raster/src space (`RenderedNotation.canvas`,\n * OSMD's own pre-map bitmap px) — passing src-space coordinates here is\n * exactly the \"classic scale gotcha\" (`extractGeometry`'s `canvas.width /\n * pageW` vs `/(contentRight+contentLeft)`, see the module doc) this component\n * exists to make impossible; don't reintroduce it at the call site.\n */\nexport function hitTestMeasureAt(layout: NotationLayout, mx: number, my: number): number | null {\n if (!layout.measures.length) return null;\n const indices = [...new Set(layout.measures.map((m) => m.index))].sort((a, b) => a - b);\n const cols = measureColumnsFromLayout(layout.measures);\n let nearest: number | null = null;\n let nearestD = Infinity;\n for (let i = 0; i < cols.length; i++) {\n const b = cols[i];\n if (mx >= b.x && mx <= b.x + b.w && my >= b.y && my <= b.y + b.h) return indices[i];\n const cx = b.x + b.w / 2;\n const cy = b.y + b.h / 2;\n const d = (mx - cx) * (mx - cx) + (my - cy) * (my - cy);\n if (d < nearestD) {\n nearestD = d;\n nearest = indices[i];\n }\n }\n return nearest;\n}\n\n/**\n * Build a live, interactive notation player: a thin packaging of the SAME\n * working path whozart's live in-browser promo player uses (see the module\n * doc). Renders the engraving ONCE (OSMD raster, via the `notation` Layer),\n * then every `setTime(tMs)` call blits the current follow window (via\n * `followBoxAt`/`vstackFollowBox` → `notationLayout`) and draws the\n * onset-locked playhead (`audioPlayheadLine`/`vstackAudioPlayheadLine`) —\n * exactly the `notation` + `scroll-cursor` Layer pair the promo/video runner\n * uses, instantiated directly instead of through the SceneSpec/timeline\n * runner (which targets a fixed-duration recorded clip, not a live,\n * caller-driven widget).\n */\nexport function createNotationPlayer(opts: CreateNotationPlayerOpts): NotationPlayer {\n const { host, musicXml, onsetsMs, noteCols, barDurMs, bars } = opts;\n const mode: NotationPlayerMode = opts.mode ?? 'vstack';\n const theme: PromoTheme = { ...DEFAULT_THEME, ...opts.theme };\n\n const canvas = document.createElement('canvas');\n canvas.style.display = 'block';\n canvas.style.width = '100%';\n canvas.style.height = '100%';\n host.appendChild(canvas);\n\n const dpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n function frameSize(): [number, number] {\n if (opts.size) return opts.size;\n const w = Math.max(1, Math.round((host.clientWidth || 1) * dpr));\n const h = Math.max(1, Math.round((host.clientHeight || 1) * dpr));\n return [w, h];\n }\n\n const [initW, initH] = frameSize();\n canvas.width = initW;\n canvas.height = initH;\n const ctx2d = canvas.getContext('2d');\n if (!ctx2d) throw new Error('createNotationPlayer: 2D canvas context unavailable');\n\n let lastTMs = 0;\n const audioClock: AudioClock = { nowMs: () => lastTMs };\n const rctx: RenderCtx = {\n ctx2d, W: initW, H: initH,\n score: scoreFromOnsets(onsetsMs),\n audioClock, theme, safeBox: safeBox(initW, initH), fps: 30,\n };\n\n const notation = notationFactory.create();\n const scrollCursor = scrollCursorFactory.create();\n\n function notationProps(rendered?: RenderedNotation): NotationProps {\n return {\n ...(rendered ? { rendered } : { xml: musicXml }),\n scrollMode: mode,\n bars,\n bandTop: opts.bandTop ?? 0,\n bandHeight: opts.bandHeight ?? rctx.H,\n };\n }\n const scrollCursorProps: ScrollCursorProps = { scrollMode: mode, barDurMs, noteCols };\n\n let renderedRn: RenderedNotation | null = null;\n let destroyed = false;\n\n const ready = (async () => {\n await notation.init(rctx, notationProps(opts.rendered));\n renderedRn = getNotationEngraving(rctx)?.rendered ?? null;\n await scrollCursor.init(rctx, scrollCursorProps);\n })();\n\n function clear(): void {\n ctx2d!.save();\n ctx2d!.setTransform(1, 0, 0, 1, 0, 0);\n ctx2d!.fillStyle = theme.paper;\n ctx2d!.fillRect(0, 0, canvas.width, canvas.height);\n ctx2d!.restore();\n }\n\n function setTime(tMs: number): void {\n if (destroyed) return;\n lastTMs = tMs;\n clear();\n notation.draw(rctx, tMs);\n scrollCursor.draw(rctx, tMs);\n }\n\n async function performResize(): Promise<void> {\n const [w, h] = frameSize();\n if (w === canvas.width && h === canvas.height) return;\n canvas.width = w;\n canvas.height = h;\n rctx.W = w;\n rctx.H = h;\n rctx.safeBox = safeBox(w, h);\n if (renderedRn) {\n // Cheap re-layout: reuses the already-rasterized bitmap (no OSMD re-run).\n await notation.init(rctx, notationProps(renderedRn));\n await scrollCursor.init(rctx, scrollCursorProps);\n }\n if (!destroyed) setTime(lastTMs);\n }\n\n function currentLayout(): NotationLayout | null {\n const eng = getNotationEngraving(rctx);\n if (!eng) return null;\n return eng.followLayoutAt ? eng.followLayoutAt(rctx, lastTMs) : eng.base;\n }\n\n const clickListeners: Array<(measureIndex: number) => void> = [];\n function onCanvasClick(e: MouseEvent): void {\n const rect = canvas.getBoundingClientRect();\n if (!(rect.width > 0) || !(rect.height > 0)) return;\n const mx = (e.clientX - rect.left) * (canvas.width / rect.width);\n const my = (e.clientY - rect.top) * (canvas.height / rect.height);\n const layout = currentLayout();\n if (!layout) return;\n const idx = hitTestMeasureAt(layout, mx, my);\n if (idx != null) for (const cb of clickListeners) cb(idx);\n }\n canvas.addEventListener('click', onCanvasClick);\n\n return {\n ready,\n setTime,\n resize(): void {\n void performResize();\n },\n onMeasureClick(cb: (measureIndex: number) => void): () => void {\n clickListeners.push(cb);\n return () => {\n const i = clickListeners.indexOf(cb);\n if (i >= 0) clickListeners.splice(i, 1);\n };\n },\n destroy(): void {\n if (destroyed) return;\n destroyed = true;\n canvas.removeEventListener('click', onCanvasClick);\n notation.dispose?.();\n scrollCursor.dispose?.();\n clickListeners.length = 0;\n if (canvas.parentNode === host) host.removeChild(canvas);\n },\n };\n}\n"],"mappings":"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","names":[]}
package/dist/promo.d.ts CHANGED
@@ -87,6 +87,20 @@ interface RenderNotationOpts {
87
87
  */
88
88
  singleRow?: boolean;
89
89
  }
90
+ declare const MAX_RASTER_AREA_PX = 12000000;
91
+ /**
92
+ * Pure clamp math (unit-tested, no DOM): given the CSS-px content dims OSMD
93
+ * is about to rasterize (`cssW`/`cssH` — the SAME layout regardless of dpr)
94
+ * and the browser's native `devicePixelRatio`, return the largest dpr `<=
95
+ * nativeDpr` whose backing-store area (`cssW*dpr × cssH*dpr`) stays within
96
+ * `capPx2`. Returns `nativeDpr` unchanged whenever the native-resolution
97
+ * raster already fits — that no-op path is what keeps desktop/short-excerpt
98
+ * rendering byte-for-byte unchanged by this clamp. Never returns less than 1
99
+ * (native CSS-px density is the floor — always legible, never sub-1x blur)
100
+ * unless `nativeDpr` itself is below 1 (not expected in a real browser, but
101
+ * handled rather than dividing by/sqrt-ing a negative).
102
+ */
103
+ declare function clampRasterDpr(cssW: number, cssH: number, nativeDpr: number, capPx2?: number): number;
90
104
  /** Render a MusicXML string to a detached canvas + geometry.
91
105
  * Browser-only (requires document + opensheetmusicdisplay dynamic import). */
92
106
  declare function renderNotation(xml: string, opts?: RenderNotationOpts): Promise<RenderedNotation>;
@@ -125,4 +139,4 @@ declare function createPromoSampler(opts?: {
125
139
  analyser?: boolean;
126
140
  }): Promise<PromoSampler>;
127
141
 
128
- export { type Box, type MeasureColumnBox, type MidiNote, type ParsedMidi, type PromoSampler, type PromoVoicing, type RenderNotationOpts, type RenderedNotation, type StaffMeasureBox, createPromoSampler, midiDurationMs, parseMidi, renderNotation };
142
+ export { type Box, MAX_RASTER_AREA_PX, type MeasureColumnBox, type MidiNote, type ParsedMidi, type PromoSampler, type PromoVoicing, type RenderNotationOpts, type RenderedNotation, type StaffMeasureBox, clampRasterDpr, createPromoSampler, midiDurationMs, parseMidi, renderNotation };
package/dist/promo.js CHANGED
@@ -301,6 +301,15 @@ function extractGeometry(osmd, canvas, inkSumThreshold) {
301
301
  return { systems: [], measures: [], measureColumns: [], content: full, noteCols: [] };
302
302
  }
303
303
  }
304
+ var MAX_RASTER_AREA_PX = 12e6;
305
+ function clampRasterDpr(cssW, cssH, nativeDpr, capPx2 = MAX_RASTER_AREA_PX) {
306
+ const safeDpr = nativeDpr > 0 ? nativeDpr : 1;
307
+ if (!(cssW > 0) || !(cssH > 0)) return safeDpr;
308
+ const nativeArea = cssW * cssH * safeDpr * safeDpr;
309
+ if (nativeArea <= capPx2) return safeDpr;
310
+ const cappedDpr = Math.sqrt(capPx2 / (cssW * cssH));
311
+ return Math.max(1, Math.min(safeDpr, cappedDpr));
312
+ }
304
313
  async function renderNotation(xml, opts) {
305
314
  const paper = opts?.paper ?? "#faf7f0";
306
315
  const inkSumThreshold = opts?.inkSumThreshold ?? 690;
@@ -332,7 +341,24 @@ async function renderNotation(xml, opts) {
332
341
  });
333
342
  }
334
343
  osmd.render();
335
- const canvas = host.querySelector("canvas");
344
+ let canvas = host.querySelector("canvas");
345
+ if (canvas && canvas.width * canvas.height > MAX_RASTER_AREA_PX) {
346
+ const nativeDpr = typeof window !== "undefined" && window.devicePixelRatio ? window.devicePixelRatio : 1;
347
+ const cssW = canvas.width / nativeDpr;
348
+ const cssH = canvas.height / nativeDpr;
349
+ const clampedDpr = clampRasterDpr(cssW, cssH, nativeDpr);
350
+ if (clampedDpr < nativeDpr) {
351
+ const desc = Object.getOwnPropertyDescriptor(window, "devicePixelRatio");
352
+ try {
353
+ Object.defineProperty(window, "devicePixelRatio", { value: clampedDpr, configurable: true });
354
+ osmd.render();
355
+ canvas = host.querySelector("canvas");
356
+ } finally {
357
+ if (desc) Object.defineProperty(window, "devicePixelRatio", desc);
358
+ else delete window.devicePixelRatio;
359
+ }
360
+ }
361
+ }
336
362
  if (canvas) {
337
363
  const { systems, measures, measureColumns, content, noteCols } = extractGeometry(osmd, canvas, inkSumThreshold);
338
364
  document.body.removeChild(host);
@@ -473,6 +499,8 @@ async function createPromoSampler(opts) {
473
499
  };
474
500
  }
475
501
  export {
502
+ MAX_RASTER_AREA_PX,
503
+ clampRasterDpr,
476
504
  createPromoSampler,
477
505
  midiDurationMs,
478
506
  parseMidi,
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 //\n // BUG FIX: the naive `canvas.width / pageW` is WRONG whenever the engraving\n // OVERFLOWS the nominal page width — which `renderSingleHorizontalStaffline`\n // (hstack) does routinely: one wide row of measures spills past pageW, and OSMD\n // sizes the canvas to the CONTENT (with its page margins), not to pageW. Using\n // canvas.width/pageW then OVER-scales every position (~5% on a 5-bar row), so\n // the cursor/highlight geometry drifts steadily RIGHT of the real noteheads and\n // DOWN in y (the same scale is applied to both axes) — accumulating to ~half a\n // bar by the end of the clip.\n //\n // OSMD renders at a single uniform scale `s` (px per OSMD unit) with symmetric\n // page margins, so `canvas.width = (contentRight + contentLeft) * s` where\n // contentLeft is the left margin. Hence `s = canvas.width / (contentRight +\n // contentLeft)`. This SELF-CALIBRATES to OSMD's zoom and, when the content fits\n // the page (vstack: contentRight ≈ pageW - margin, contentLeft = margin), it\n // reduces to canvas.width/pageW — so it's correct for both layouts. Falls back\n // to pageW if the content extent can't be measured.\n let contentRightU = -Infinity, contentLeftU = Infinity;\n for (const staves of (graphic?.MeasureList ?? []) as any[][]) {\n for (const m of (staves ?? [])) {\n const ps = m?.PositionAndShape;\n if (!ps?.AbsolutePosition || !ps?.Size || !(ps.Size.width > 0.1)) continue;\n contentRightU = Math.max(contentRightU, ps.AbsolutePosition.x + ps.Size.width);\n contentLeftU = Math.min(contentLeftU, ps.AbsolutePosition.x);\n }\n }\n const spanU = Number.isFinite(contentRightU) && Number.isFinite(contentLeftU)\n ? contentRightU + contentLeftU\n : pageW;\n const f = canvas.width / (spanU > 0 ? spanU : 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"]}
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 //\n // BUG FIX: the naive `canvas.width / pageW` is WRONG whenever the engraving\n // OVERFLOWS the nominal page width — which `renderSingleHorizontalStaffline`\n // (hstack) does routinely: one wide row of measures spills past pageW, and OSMD\n // sizes the canvas to the CONTENT (with its page margins), not to pageW. Using\n // canvas.width/pageW then OVER-scales every position (~5% on a 5-bar row), so\n // the cursor/highlight geometry drifts steadily RIGHT of the real noteheads and\n // DOWN in y (the same scale is applied to both axes) — accumulating to ~half a\n // bar by the end of the clip.\n //\n // OSMD renders at a single uniform scale `s` (px per OSMD unit) with symmetric\n // page margins, so `canvas.width = (contentRight + contentLeft) * s` where\n // contentLeft is the left margin. Hence `s = canvas.width / (contentRight +\n // contentLeft)`. This SELF-CALIBRATES to OSMD's zoom and, when the content fits\n // the page (vstack: contentRight ≈ pageW - margin, contentLeft = margin), it\n // reduces to canvas.width/pageW — so it's correct for both layouts. Falls back\n // to pageW if the content extent can't be measured.\n let contentRightU = -Infinity, contentLeftU = Infinity;\n for (const staves of (graphic?.MeasureList ?? []) as any[][]) {\n for (const m of (staves ?? [])) {\n const ps = m?.PositionAndShape;\n if (!ps?.AbsolutePosition || !ps?.Size || !(ps.Size.width > 0.1)) continue;\n contentRightU = Math.max(contentRightU, ps.AbsolutePosition.x + ps.Size.width);\n contentLeftU = Math.min(contentLeftU, ps.AbsolutePosition.x);\n }\n }\n const spanU = Number.isFinite(contentRightU) && Number.isFinite(contentLeftU)\n ? contentRightU + contentLeftU\n : pageW;\n const f = canvas.width / (spanU > 0 ? spanU : 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// ─── Mobile canvas-area clamp ──────────────────────────────────────────────\n//\n// WHY THIS EXISTS: OSMD's canvas backend (opensheetmusicdisplay's vendored\n// VexFlow-canvas resize()) multiplies its CSS-px backing-store dims by\n// `window.devicePixelRatio` with NO area check — it only guards each\n// DIMENSION against a 32767px ceiling (`SanitizeCanvasDims`), applied BEFORE\n// the devicePixelRatio multiply, so the guard does nothing for the failure\n// mode that actually bites: a long `vstack` (page-wrap) engraving stacks one\n// system per row, so total CSS height grows with piece length — a 65-bar\n// piece easily reaches ~14,000 CSS px tall — and at a mobile `devicePixelRatio`\n// of 2–3 that height alone lands near or past 32767 px in device pixels, while\n// the resulting AREA (width_px × height_px) blows straight through iOS\n// Safari's well-documented ~16,777,216px² (4096×4096) canvas backing-store\n// limit. Past that limit iOS Safari does not throw or warn — it silently\n// clears/no-ops the canvas, so `renderNotation` returns what LOOKS like a\n// valid non-null `RenderedNotation` (real dims, real geometry from\n// `extractGeometry`) whose `canvas` is actually blank. That is the root cause\n// of the /bach/play \"As written\" mobile-blank bug (E7M — 2026-08-10): the\n// FULL 65-bar Chopin piece rasterizes to 1680×41936 device px (~70.5M px²,\n// 4.2× the iOS cap) at devicePixelRatio 3. Confirmed via a raw-canvas-op trace\n// (Playwright, iPhone profile) — see docs/superpowers/sdd/written-mobile-blank-report.md.\n//\n// FIX: after OSMD's normal (native-dpr) render, check the resulting canvas's\n// device-px AREA against a conservative cap comfortably under iOS's real\n// limit. If it fits, this is a complete no-op (byte-identical to before this\n// change — desktop and any short/promo excerpt never re-renders). If it\n// doesn't, re-render ONCE more at a reduced effective devicePixelRatio\n// (`clampRasterDpr`, pure/testable) that brings the SAME CSS-px layout back\n// under the cap — the engraving loses some sharpness on very long pieces\n// (fewer device px per notehead) but stays fully legible and, critically,\n// actually paints. This is a resolution clamp, not a content/layout change:\n// `osmd`'s CSS-px page layout (line breaks, systems, bar placement) is\n// unaffected — only the backing-store pixel density is reduced — so\n// `extractGeometry`'s CSS-px-independent geometry (systems/measures/content\n// boxes, all read in CANVAS px post-render) still lines up with the smaller\n// canvas.\n//\n// HOW: OSMD's canvas backend reads `window.devicePixelRatio` directly (no\n// public hook to override per-call), so this temporarily overrides the\n// global `devicePixelRatio` accessor for the DURATION OF ONE SYNCHRONOUS\n// `osmd.render()` CALL ONLY, then restores the original descriptor in a\n// `finally`. Safe because `osmd.render()` is called (both here and in the\n// pre-existing first pass above) without `await` — canvas-backend rendering\n// is synchronous — so no other code can observe the override mid-flight\n// (single-threaded JS; nothing yields between the override and the restore).\nexport const MAX_RASTER_AREA_PX = 12_000_000;\n\n/**\n * Pure clamp math (unit-tested, no DOM): given the CSS-px content dims OSMD\n * is about to rasterize (`cssW`/`cssH` — the SAME layout regardless of dpr)\n * and the browser's native `devicePixelRatio`, return the largest dpr `<=\n * nativeDpr` whose backing-store area (`cssW*dpr × cssH*dpr`) stays within\n * `capPx2`. Returns `nativeDpr` unchanged whenever the native-resolution\n * raster already fits — that no-op path is what keeps desktop/short-excerpt\n * rendering byte-for-byte unchanged by this clamp. Never returns less than 1\n * (native CSS-px density is the floor — always legible, never sub-1x blur)\n * unless `nativeDpr` itself is below 1 (not expected in a real browser, but\n * handled rather than dividing by/sqrt-ing a negative).\n */\nexport function clampRasterDpr(\n cssW: number,\n cssH: number,\n nativeDpr: number,\n capPx2: number = MAX_RASTER_AREA_PX,\n): number {\n const safeDpr = nativeDpr > 0 ? nativeDpr : 1;\n if (!(cssW > 0) || !(cssH > 0)) return safeDpr;\n const nativeArea = cssW * cssH * safeDpr * safeDpr;\n if (nativeArea <= capPx2) return safeDpr;\n const cappedDpr = Math.sqrt(capPx2 / (cssW * cssH));\n return Math.max(1, Math.min(safeDpr, cappedDpr));\n}\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 let canvas = host.querySelector('canvas');\n\n // Mobile canvas-area clamp — see the module doc above `clampRasterDpr`.\n // No-ops (skips straight through) whenever the native-dpr raster already\n // fits the cap, which is every desktop render and every short/promo\n // excerpt — only a long vstack piece at a high mobile dpr re-renders.\n if (canvas && canvas.width * canvas.height > MAX_RASTER_AREA_PX) {\n const nativeDpr = typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1;\n const cssW = canvas.width / nativeDpr;\n const cssH = canvas.height / nativeDpr;\n const clampedDpr = clampRasterDpr(cssW, cssH, nativeDpr);\n if (clampedDpr < nativeDpr) {\n const desc = Object.getOwnPropertyDescriptor(window, 'devicePixelRatio');\n try {\n Object.defineProperty(window, 'devicePixelRatio', { value: clampedDpr, configurable: true });\n osmd.render();\n canvas = host.querySelector('canvas');\n } finally {\n if (desc) Object.defineProperty(window, 'devicePixelRatio', desc);\n else delete (window as unknown as Record<string, unknown>).devicePixelRatio;\n }\n }\n }\n\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"]}