@real-music-packages/web-core 0.9.7 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,950 @@
1
+ import { PromoTheme, SafeBox, RecordOpts, Scene } from '../video.js';
2
+ import { RenderedNotation, Box, StaffMeasureBox, MeasureColumnBox } from '../promo.js';
3
+
4
+ interface ScoreNote {
5
+ /** MIDI note number (middle C = 60). */
6
+ pitchMidi: number;
7
+ /** Diatonic letter name: C D E F G A B. */
8
+ step: string;
9
+ /** Chromatic alteration in semitones: -1 flat, +1 sharp, 0 natural, ±2 double. */
10
+ alter: number;
11
+ /** Scientific octave (middle C = C4). */
12
+ octave: number;
13
+ /** Onset on the linear playback clock, in ms (repeats expanded). */
14
+ onsetMs: number;
15
+ /** Sounding duration in ms (tie chains merged). */
16
+ durMs: number;
17
+ /** Staff index within the whole sheet (0-based). */
18
+ staff: number;
19
+ /** Voice id within the part. */
20
+ voice: number;
21
+ /** Performing hand: grand-staff top -> "R", bottom -> "L" (see hand-inference stub). */
22
+ hand: 'L' | 'R';
23
+ /** Lyric syllable attached to this note, if any. */
24
+ lyric?: string;
25
+ /** Fingering digit attached to this note, if any. */
26
+ fingering?: number;
27
+ }
28
+ /**
29
+ * Tempo map. v1 is a single constant tempo (one segment at t=0). The piecewise
30
+ * shape is the documented seam for rubato / multiple `<sound tempo>` — see the
31
+ * tempo-map stub note in scoreFromMusicXML.
32
+ */
33
+ interface TempoMap {
34
+ /** Where the tempo came from: the XML's notated tempo, the fallback, or an override. */
35
+ source: 'xml' | 'fallback' | 'override';
36
+ /** Piecewise-constant segments, ordered by onset. v1 always has exactly one (at 0). */
37
+ segments: Array<{
38
+ atMs: number;
39
+ bpm: number;
40
+ }>;
41
+ }
42
+ interface Score {
43
+ notes: ScoreNote[];
44
+ tempoMap: TempoMap;
45
+ /** End of the last sounding note, in ms. */
46
+ durationMs: number;
47
+ key?: string;
48
+ timeSig?: string;
49
+ title?: string;
50
+ composer?: string;
51
+ }
52
+ interface ScoreFromMusicXMLOpts {
53
+ /** bpm to use when the XML has no notated tempo (DefaultStartTempoInBpm === 0). Default 100. */
54
+ tempoFallback?: number;
55
+ /** Force this bpm regardless of the XML's notated tempo (per-recipe override). */
56
+ tempoOverride?: number;
57
+ /**
58
+ * Provide the OSMD instance. Defaults to a literal `import('opensheetmusicdisplay')`
59
+ * + a detached div (works in a browser, or in Node after `setupHeadlessDom()`).
60
+ * Inject for tests or non-DOM environments.
61
+ */
62
+ osmdFactory?: () => any;
63
+ }
64
+ /**
65
+ * Parse MusicXML into a timed, typed Score via OSMD's source model.
66
+ *
67
+ * @param xml MusicXML document (uncompressed string; unzip .mxl first).
68
+ * @param opts tempoFallback / tempoOverride / osmdFactory.
69
+ */
70
+ declare function scoreFromMusicXML(xml: string, opts?: ScoreFromMusicXMLOpts): Promise<Score>;
71
+
72
+ /**
73
+ * Install jsdom globals + a fake 2D canvas context so OSMD can `load()` a score
74
+ * headlessly. Idempotent. Call once before constructing an OSMD instance in Node.
75
+ *
76
+ * No-ops if `window`/`document` already exist (e.g. a real browser, or a vitest
77
+ * jsdom environment), so it is safe to call unconditionally.
78
+ */
79
+ declare function setupHeadlessDom(): Promise<void>;
80
+
81
+ /**
82
+ * Audio clock the runner exposes to layers. `nowMs` is the current playback
83
+ * position in milliseconds (audio-clock-driven, NOT wall-clock). During an
84
+ * offline/deterministic render the runner supplies the frame time directly, so
85
+ * `nowMs` and the `tMs` passed to `draw` agree.
86
+ */
87
+ interface AudioClock {
88
+ /** Current playback position in ms. */
89
+ nowMs(): number;
90
+ }
91
+ /**
92
+ * Everything a layer needs to draw a frame. Constructed once per render and
93
+ * passed to every `init`/`draw`. World coordinates: layers draw in the frame's
94
+ * pixel space (0,0 top-left, W×H); the camera primitive (./camera) applies any
95
+ * pan/zoom transform to `ctx2d` BEFORE the layer's `draw` runs, so a layer never
96
+ * re-derives the viewport.
97
+ */
98
+ interface RenderCtx {
99
+ /** The shared 2D context all layers draw onto. */
100
+ ctx2d: CanvasRenderingContext2D;
101
+ /** Frame width in px. */
102
+ W: number;
103
+ /** Frame height in px. */
104
+ H: number;
105
+ /** The parsed Score (timing/pitch/hands/lyrics). May be undefined for
106
+ * non-musical scenes (a pure hook/CTA card). */
107
+ score?: Score;
108
+ /** Audio playback clock. */
109
+ audioClock: AudioClock;
110
+ /** Per-app theme tokens (colours/fonts/brand). */
111
+ theme: PromoTheme;
112
+ /** Phone-safe content rectangle (./video safeBox) — layers anchor to this
113
+ * instead of re-deriving insets. */
114
+ safeBox: SafeBox;
115
+ /** Capture frame rate. */
116
+ fps: number;
117
+ }
118
+ /**
119
+ * A composable, time-synced render component.
120
+ *
121
+ * @typeParam P the layer's prop type (what a SceneSpec passes as `p`).
122
+ */
123
+ interface Layer<P = unknown> {
124
+ /** Stable identity, e.g. "notation" | "falling-notes" | "caption". */
125
+ readonly key: string;
126
+ /**
127
+ * One-time setup: load assets, lay out, rasterize an offscreen bitmap, etc.
128
+ * Runs before the first captured frame. May be async (e.g. font/IR load).
129
+ */
130
+ init(ctx: RenderCtx, props: P): void | Promise<void>;
131
+ /**
132
+ * Per-frame draw. MUST be cheap and a pure function of `tMs` (no rAF / wall
133
+ * clock). `tMs` is the absolute playback time in ms.
134
+ */
135
+ draw(ctx: RenderCtx, tMs: number): void;
136
+ /** Optional teardown (free bitmaps / audio nodes). */
137
+ dispose?(): void;
138
+ }
139
+ /**
140
+ * A layer factory keyed by name, with a runtime prop validator so the runner /
141
+ * pre-render gate can reject an unknown prop before capture (spec: "unknown
142
+ * layer/prop → fail fast"). `validateProps` returns an array of human-readable
143
+ * errors ([] = valid).
144
+ */
145
+ interface LayerFactory<P = unknown> {
146
+ key: string;
147
+ create(): Layer<P>;
148
+ /** Validate a SceneSpec's `p` object. Return [] when valid. */
149
+ validateProps(props: unknown): string[];
150
+ }
151
+
152
+ declare const clamp: (x: number, lo: number, hi: number) => number;
153
+ declare const lerp: (a: number, b: number, t: number) => number;
154
+ /** Inverse-lerp: where does `x` sit in [a,b] as 0..1 (clamped, a===b -> 0). */
155
+ declare const invLerp: (a: number, b: number, x: number) => number;
156
+ type Easing = (t: number) => number;
157
+ declare const linear: Easing;
158
+ /** Smoothstep ease-in-out. */
159
+ declare const easeInOut: Easing;
160
+ declare const easeIn: Easing;
161
+ declare const easeOut: Easing;
162
+
163
+ /** A rectangle in WORLD coordinates (the layers' natural pixel space). */
164
+ interface Rect {
165
+ x: number;
166
+ y: number;
167
+ w: number;
168
+ h: number;
169
+ }
170
+ /** A camera pose: which world point sits at the viewport centre, and the zoom. */
171
+ interface CameraState {
172
+ /** World-space centre x. */
173
+ cx: number;
174
+ /** World-space centre y. */
175
+ cy: number;
176
+ /** Zoom factor (>1 = zoomed in). */
177
+ zoom: number;
178
+ }
179
+ /** A 2D affine transform `[a,b,c,d,e,f]` (matches CanvasRenderingContext2D.setTransform). */
180
+ type Affine = [number, number, number, number, number, number];
181
+ /**
182
+ * The world→viewport transform for a camera pose. We use a uniform scale = zoom
183
+ * and translate so (cx,cy) world maps to (W/2,H/2) viewport.
184
+ * world point (x,y) -> ( (x-cx)*zoom + W/2 , (y-cy)*zoom + H/2 ).
185
+ */
186
+ declare function cameraTransform(cam: CameraState, W: number, H: number): Affine;
187
+ /** Map a world point through a camera pose to viewport pixels. */
188
+ declare function worldToViewport(cam: CameraState, W: number, H: number, x: number, y: number): {
189
+ x: number;
190
+ y: number;
191
+ };
192
+ /**
193
+ * The camera pose that frames `rect` to fill the W×H viewport (contain-fit:
194
+ * whole rect visible, uniform zoom). `pad` (0..1) leaves margin around the rect.
195
+ */
196
+ declare function frameRect(rect: Rect, W: number, H: number, pad?: number): CameraState;
197
+ /** Interpolate between two camera poses (for pan/zoom moves). */
198
+ declare function lerpCamera(a: CameraState, b: CameraState, t01: number, ease?: Easing): CameraState;
199
+ /**
200
+ * A Ken-Burns move: slow drift from `from` to `to` over a segment. `at` is the
201
+ * segment-relative position 0..1. Pure — returns the pose for that t.
202
+ */
203
+ declare function kenBurns(from: CameraState, to: CameraState, at01: number): CameraState;
204
+ /**
205
+ * Apply a camera pose to a 2D context (sets the transform). Layers drawn after
206
+ * this render in world coords and appear panned/zoomed. Pair with ctx.save()
207
+ * /ctx.restore() in the runner so layers can't leak the transform.
208
+ */
209
+ declare function applyToContext(ctx: CanvasRenderingContext2D, cam: CameraState, W: number, H: number): void;
210
+ /** The identity (no pan/zoom) camera: world == viewport. */
211
+ declare function identityCamera(W: number, H: number): CameraState;
212
+
213
+ /** A timeline endpoint: a number (ms? no — seconds), "end", or "end-N" (N s before end). */
214
+ type TimeAnchor = number | 'end' | `end-${number}`;
215
+ interface SpecLayer {
216
+ /** Registry key. */
217
+ k: string;
218
+ /** Props passed to the layer's init(). */
219
+ p?: unknown;
220
+ }
221
+ interface TimelineSegment {
222
+ /** [start, end] in SECONDS; end may be "end" / "end-N". */
223
+ at: [number, TimeAnchor];
224
+ layers: SpecLayer[];
225
+ }
226
+ interface SceneSpec {
227
+ /** [width, height] px. */
228
+ size: [number, number];
229
+ /** Theme key (resolved to a PromoTheme by the host) OR an inline theme. */
230
+ theme: string;
231
+ /** "audio" = clip length follows the audio; "fixed" = use `durationSec`. */
232
+ durationMode: 'audio' | 'fixed';
233
+ /** Required when durationMode === "fixed". */
234
+ durationSec?: number;
235
+ timeline: TimelineSegment[];
236
+ audio?: {
237
+ voicing?: string;
238
+ [k: string]: unknown;
239
+ };
240
+ /** Capture frame rate. Default 30. */
241
+ fps?: number;
242
+ }
243
+ /** Resolve a TimeAnchor to absolute seconds given the clip's total length. */
244
+ declare function resolveAnchor(anchor: TimeAnchor, totalSec: number): number;
245
+ interface ResolvedSegment {
246
+ startMs: number;
247
+ endMs: number;
248
+ layers: SpecLayer[];
249
+ }
250
+ /** Resolve every segment's [start,end] to ms against the total clip length. */
251
+ declare function resolveTimeline(spec: SceneSpec, totalSec: number): ResolvedSegment[];
252
+ /** The visual timeline length in ms = the latest resolved segment end. */
253
+ declare function visualTimelineMs(resolved: ResolvedSegment[]): number;
254
+ interface BuiltScene {
255
+ W: number;
256
+ H: number;
257
+ fps: number;
258
+ durationMs: number;
259
+ /** Draw the whole composite at absolute time `tMs` onto `ctx2d`. Deterministic
260
+ * (pure fn of tMs). Used by tests, golden frames, and the recordScenes wrapper. */
261
+ renderFrame(ctx2d: CanvasRenderingContext2D, tMs: number): void;
262
+ /** Free every layer's resources. */
263
+ dispose(): void;
264
+ /** The resolved timeline (for the gate / diagnostics). */
265
+ resolved: ResolvedSegment[];
266
+ }
267
+ interface BuildSceneOpts {
268
+ spec: SceneSpec;
269
+ theme: PromoTheme;
270
+ /** The parsed Score (timing source). Optional for non-musical specs. */
271
+ score?: Score;
272
+ /** Total clip length in seconds. With durationMode "audio" pass the audio
273
+ * length; with "fixed" it defaults to spec.durationSec. */
274
+ totalSec: number;
275
+ /** Override the camera pose per absolute tMs (pan/zoom director). Identity by
276
+ * default. */
277
+ camera?: (tMs: number) => CameraState;
278
+ }
279
+ /**
280
+ * Instantiate + init every layer in the spec and return a deterministic renderer.
281
+ * Throws if a referenced layer key isn't registered or its props don't validate
282
+ * (so build failures surface before capture — the gate also re-checks).
283
+ */
284
+ declare function buildScene(opts: BuildSceneOpts): Promise<BuiltScene>;
285
+ interface RecordSceneSpecOpts {
286
+ built: BuiltScene;
287
+ audioStream: MediaStream;
288
+ background?: string;
289
+ onProgress?: RecordOpts['onProgress'];
290
+ /** Inject the recorder for tests. Defaults to recordScenes. */
291
+ record?: (scenes: Scene[], o: RecordOpts) => Promise<Blob>;
292
+ }
293
+ /**
294
+ * Capture a built scene to a Blob by wrapping the whole timeline as a SINGLE
295
+ * composite Scene and handing it to recordScenes — so audio sync, captureStream,
296
+ * and MediaRecorder all come from the existing, working path. recordScenes
297
+ * clears to `background` each frame and gives us a 0..1 t; we expand it back to
298
+ * absolute ms and call renderFrame.
299
+ */
300
+ declare function recordSceneSpec(opts: RecordSceneSpecOpts): Promise<Blob>;
301
+
302
+ /** Register (or replace) a layer factory under its key. */
303
+ declare function registerLayer(factory: LayerFactory<any>): void;
304
+ /** Look up a factory by key, or undefined if not registered. */
305
+ declare function getLayerFactory(key: string): LayerFactory<any> | undefined;
306
+ /** All registered layer keys (for diagnostics / gate messages). */
307
+ declare function registeredKeys(): string[];
308
+
309
+ interface HighlightRegion {
310
+ /** World x of the band's left edge. */
311
+ x: number;
312
+ /** World y of the band's top edge. */
313
+ y: number;
314
+ /** Band width (world px). */
315
+ w: number;
316
+ /** Band height (world px). */
317
+ h: number;
318
+ /** Show from this time (ms). */
319
+ inMs: number;
320
+ /** Hide after this time (ms). */
321
+ outMs: number;
322
+ /** Fade in/out duration (ms). Default 120. */
323
+ fadeMs?: number;
324
+ /** Fill colour (defaults to a translucent accent at draw time). */
325
+ color?: string;
326
+ }
327
+ /**
328
+ * Highlight intensity 0..1 at time `tMs`: 0 outside the window, ramping to 1
329
+ * across `fadeMs` at each edge, flat 1 in the middle. Pure.
330
+ */
331
+ declare function highlightIntensity(region: HighlightRegion, tMs: number): number;
332
+ /**
333
+ * Compute the world x-range covering a set of note onsets that fall inside a
334
+ * time window, given a time→x mapping. Returns null if none qualify. Useful for
335
+ * "spotlight the bar that's sounding now" without a layer re-deriving geometry.
336
+ */
337
+ declare function noteSetXRange(onsetsMs: number[], windowMs: [number, number], timeToX: (ms: number) => number): {
338
+ x: number;
339
+ w: number;
340
+ } | null;
341
+ /**
342
+ * Draw a highlight band for the current time. No-op when intensity is 0, so it
343
+ * is safe to call every frame. Draws in WORLD coords (the camera transform, if
344
+ * any, is already on the context).
345
+ */
346
+ declare function drawHighlight(ctx: CanvasRenderingContext2D, region: HighlightRegion, tMs: number, accent: string): void;
347
+
348
+ interface CaptionCue {
349
+ text: string;
350
+ /** Show from this time (ms). */
351
+ inMs: number;
352
+ /** Hide after this time (ms). */
353
+ outMs: number;
354
+ }
355
+ type CaptionScript = CaptionCue[];
356
+ /** The cue active at `tMs` (last one whose window contains t), or null. */
357
+ declare function activeCue(script: CaptionScript, tMs: number): CaptionCue | null;
358
+ /** Opacity 0..1 for a cue at `tMs` (short fade at both edges). */
359
+ declare function cueOpacity(cue: CaptionCue, tMs: number, fadeMs?: number): number;
360
+ interface CaptionStyle {
361
+ /** Font size in px. Default 44. */
362
+ size?: number;
363
+ /** Pinned vertical position as a fraction of the safe-box height from its
364
+ * top. Default 0.92 (near the bottom of the safe area). */
365
+ yFrac?: number;
366
+ /** Draw a translucent backing pill for legibility. Default true. */
367
+ pill?: boolean;
368
+ }
369
+ /**
370
+ * Draw the active caption (if any) inside the safe box. Pure function of tMs.
371
+ * No-op when no cue is active. Drawn in VIEWPORT space (call after the camera
372
+ * transform has been reset to identity) so captions stay pinned to the screen.
373
+ */
374
+ declare function drawCaption(ctx: CanvasRenderingContext2D, script: CaptionScript, tMs: number, safe: SafeBox, theme: PromoTheme, style?: CaptionStyle): void;
375
+
376
+ /** One scheduled audio event, in seconds RELATIVE to the schedule start. */
377
+ interface AudioEvent {
378
+ /** Offset from schedule start, seconds. */
379
+ atSec: number;
380
+ /** Pitch (note name or frequency) for tonal hits; omitted for noise clicks. */
381
+ note?: string | number;
382
+ /** Duration, seconds. Default short. */
383
+ durSec?: number;
384
+ /** Linear gain 0..1. Default 1. */
385
+ gain?: number;
386
+ /** Tag for tests/debugging: "count" | "click" | "drone". */
387
+ kind: 'count' | 'click' | 'drone';
388
+ }
389
+ interface CountInOpts {
390
+ /** Beats to count (default 4 — a full bar of 4/4). */
391
+ beats?: number;
392
+ /** Tempo for the count, bpm. */
393
+ bpm: number;
394
+ /** Accent the downbeat (beat 1) with a higher pitch. Default true. */
395
+ accentDownbeat?: boolean;
396
+ }
397
+ /**
398
+ * The count-in schedule: one click per beat, starting at t=0, ending exactly on
399
+ * the downbeat of bar 1 (i.e. the music starts at `beats * 60/bpm` seconds).
400
+ * Pure.
401
+ */
402
+ declare function countInSchedule(opts: CountInOpts): AudioEvent[];
403
+ /** Seconds after t=0 that the music should start, for a given count-in. */
404
+ declare function countInLeadSec(opts: CountInOpts): number;
405
+ interface ClickTrackOpts {
406
+ bpm: number;
407
+ /** Total duration to fill with clicks, seconds. */
408
+ durationSec: number;
409
+ /** Beats per bar, for downbeat accents. Default 4. */
410
+ beatsPerBar?: number;
411
+ /** Start offset, seconds. Default 0. */
412
+ startSec?: number;
413
+ }
414
+ /** A click on every beat across `durationSec`, accenting each bar's downbeat. */
415
+ declare function clickTrackSchedule(opts: ClickTrackOpts): AudioEvent[];
416
+ interface DroneOpts {
417
+ /** Root pitch of the pad (e.g. "C2"). */
418
+ root: string;
419
+ /** Pad duration, seconds. */
420
+ durationSec: number;
421
+ /** Add a perfect fifth above the root. Default true. */
422
+ fifth?: boolean;
423
+ /** Bed gain 0..1. Default 0.15 (sits well under the melody). */
424
+ gain?: number;
425
+ }
426
+ /** A sustained root (+fifth) pad for the whole clip. */
427
+ declare function droneSchedule(opts: DroneOpts): AudioEvent[];
428
+ interface DuckWindow {
429
+ /** Voice-over window start, seconds. */
430
+ startSec: number;
431
+ /** Voice-over window end, seconds. */
432
+ endSec: number;
433
+ }
434
+ /**
435
+ * Gain envelope for a music/bed bus that ducks under voice-over windows. Returns
436
+ * the linear gain 0..1 at time `tSec`: `floor` inside a window (after a short
437
+ * attack ramp), 1 outside (after a release ramp). Pure — feed it to a Tone
438
+ * Gain's `.gain.value` per frame, or sample it to build a ramp automation.
439
+ */
440
+ declare function duckGainAt(windows: DuckWindow[], tSec: number, floor?: number, rampSec?: number): number;
441
+ /**
442
+ * Trigger a computed schedule on a Tone instrument at `startTime` (audio-clock
443
+ * seconds, e.g. `Tone.now()` or sampler.audioNow()). Thin glue over the existing
444
+ * sampler/synth — no new mastering bus. Tonal events use triggerAttackRelease;
445
+ * the instrument is whatever the caller wires into the promo mastering chain.
446
+ *
447
+ * Returns the number of events fired (handy for tests/gate sanity).
448
+ */
449
+ declare function applySchedule(instrument: {
450
+ triggerAttackRelease: (note: any, dur: any, time?: any, velocity?: any) => void;
451
+ }, schedule: AudioEvent[], startTime: number): number;
452
+
453
+ interface GateError {
454
+ check: 'av-duration' | 'placement' | 'fonts' | 'spec' | 'output';
455
+ message: string;
456
+ }
457
+ /** A position a layer reports placing (for the placement-sanity check). */
458
+ interface Placement {
459
+ /** Label for diagnostics (e.g. "note@1200ms"). */
460
+ label: string;
461
+ x: number;
462
+ y: number;
463
+ /** Whether this placement is REQUIRED to be inside the safe box. */
464
+ mustBeSafe?: boolean;
465
+ }
466
+ interface OutputProbe {
467
+ /** Frames the capture actually produced. */
468
+ frames: number;
469
+ /** Audio tracks present (ffprobe a:0…). */
470
+ audioTracks: number;
471
+ /** Optional measured duration in ms (ffprobe), for a secondary A/V check. */
472
+ durationMs?: number;
473
+ }
474
+ interface GateInput {
475
+ spec: SceneSpec;
476
+ /** Audio length in ms (the sounding length the capture targets). */
477
+ audioMs: number;
478
+ /** Total clip length in seconds used to resolve the timeline. */
479
+ totalSec: number;
480
+ /** Frame size for bounds + safe checks. */
481
+ W: number;
482
+ H: number;
483
+ safeBox: SafeBox;
484
+ /** Whether safezone enforcement is on for this clip. */
485
+ safezone: boolean;
486
+ /** Frame rate (for the expected-frame-count math). Default 30. */
487
+ fps?: number;
488
+ /** Required placements a layer reported (optional; pass [] if none). */
489
+ placements?: Placement[];
490
+ /**
491
+ * Font-readiness hook. The smvp recipe passes `() => document.fonts.ready
492
+ * .then(() => true)` (after loadBrandFonts). Default: assume loaded (true) so
493
+ * headless unit runs don't fail on a missing document.
494
+ */
495
+ fontsReady?: () => boolean | Promise<boolean>;
496
+ /**
497
+ * Output probe hook. The smvp recipe runs ffprobe on the produced file and
498
+ * passes the result. Omit to skip the output check (e.g. a pre-capture gate
499
+ * pass that only validates the spec).
500
+ */
501
+ output?: OutputProbe;
502
+ }
503
+ /**
504
+ * Run the gate. Returns the list of failures ([] = passes). Pure except for the
505
+ * optional async `fontsReady` hook. Call before capture (spec + A/V + placement)
506
+ * and again after capture with `output` set (real-output check).
507
+ */
508
+ declare function runGate(input: GateInput): Promise<GateError[]>;
509
+ /** Convenience: run the gate and throw a single aggregated error on failure. */
510
+ declare function assertGate(input: GateInput): Promise<void>;
511
+
512
+ interface BackgroundProps {
513
+ /** "paper" (theme.paper) | "ink" (theme.ink) | a literal CSS colour. */
514
+ style?: string;
515
+ }
516
+ interface CaptionProps {
517
+ /** Timed cues (in/out in ms relative to the clip start). */
518
+ script: CaptionScript;
519
+ size?: number;
520
+ yFrac?: number;
521
+ }
522
+
523
+ interface NotationProps {
524
+ /** Engraving system: "grand" (two staves) or "single". Informational for v1 —
525
+ * the layout is driven by the rasterized bitmap's geometry either way. */
526
+ system?: 'grand' | 'single';
527
+ /** Extra scale applied to the band height the notation fits into. Default 1. */
528
+ scale?: number;
529
+ /**
530
+ * A pre-rendered notation (test/headless injection). When omitted, init()
531
+ * calls renderNotation(xml). One of `rendered` or `xml` is required.
532
+ */
533
+ rendered?: RenderedNotation;
534
+ /** MusicXML to engrave (browser path). Ignored when `rendered` is given. */
535
+ xml?: string;
536
+ /** Bar range [from,to] forwarded to renderNotation (RSR drawFrom/drawUpTo). */
537
+ bars?: [number, number];
538
+ /** Top y of the notation band (screen px). Default safeBox.top. */
539
+ bandTop?: number;
540
+ /** Height of the notation band (screen px). Default safeBox.bottom - bandTop. */
541
+ bandHeight?: number;
542
+ }
543
+ declare const notationFactory: LayerFactory<NotationProps>;
544
+
545
+ interface ScrollCursorProps {
546
+ /** Bars visible in the follow window. Default 2 (RSR FOLLOW_BARS). Informational
547
+ * for v1 — the geometry uses the module constant unless overridden here. */
548
+ followBars?: number;
549
+ /** Opening-zoom duration in ms before the music/cursor start (RSR INTRO_MS=900).
550
+ * During this lead-in camProgress is held at 0. Default 900. */
551
+ openingZoomMs?: number;
552
+ /** Total music length in ms (RSR musicMs). Required to pace camProgress + the
553
+ * cursor against the audio clock. */
554
+ musicMs: number;
555
+ /** Cursor stroke colour. Defaults to theme.accent. */
556
+ color?: string;
557
+ }
558
+ declare const scrollCursorFactory: LayerFactory<ScrollCursorProps>;
559
+
560
+ /** measures (= rows) visible at once in the follow window. */
561
+ declare const FOLLOW_BARS = 2;
562
+ /** breathing room around the follow window. */
563
+ declare const FOLLOW_PAD = 1.06;
564
+ /** Cubic ease-in-out, t in [0,1] (RSR port of stave-video core/camera.py). */
565
+ declare function cubicEaseInOut(t: number): number;
566
+ /** Linear interpolation of two boxes. (RSR lerpBox) */
567
+ declare function lerpBox(a: Box, b: Box, e: number): Box;
568
+ /** Smallest crop of `aspect` containing `box` padded by `pad`, clamped to the
569
+ * canvas. (RSR cropAroundBox) */
570
+ declare function cropAroundBox(box: Box, aspect: number, pad: number, cw: number, ch: number): Box;
571
+ /** Union (canvas coords) of all staff measure boxes with index in [lo,hi).
572
+ * (RSR measureSpanBox) */
573
+ declare function measureSpanBox(rn: RenderedNotation, lo: number, hi: number): Box | null;
574
+ /** Union of the index-0 measure boxes — the opening focal. (RSR firstMeasureBox) */
575
+ declare function firstMeasureBox(rn: RenderedNotation): Box | null;
576
+ /** Number of distinct measures in the notation. (RSR measureCount) */
577
+ declare function measureCount(rn: RenderedNotation): number;
578
+ /** The follow window (canvas coords) for a continuous measure-start position,
579
+ * spanning FOLLOW_BARS and lerping between adjacent windows. (RSR followBoxAt) */
580
+ declare function followBoxAt(rn: RenderedNotation, posMeasures: number): Box | null;
581
+ /**
582
+ * The continuous follow-window START measure for an audio-clock progress 0..1.
583
+ * (RSR +page.svelte:738-744 — hold the current pair, ease-scroll over the last
584
+ * third of each bar.) Returns the windowStart fed to followBoxAt.
585
+ */
586
+ declare function followWindowStart(nBars: number, camProgress01: number): number;
587
+ /** The dest rect a notation frame is drawn into, on screen. */
588
+ interface NotationRect {
589
+ dx: number;
590
+ dy: number;
591
+ dw: number;
592
+ dh: number;
593
+ }
594
+ /** Geometry result of laying out the notation for one frame: which canvas `src`
595
+ * rect is blitted to which screen `dest` rect, plus the mapped boxes. */
596
+ interface NotationLayout {
597
+ /** Canvas-space crop blitted this frame. */
598
+ src: Box;
599
+ /** Screen-space dest rect. */
600
+ rect: NotationRect;
601
+ /** Per-row system boxes mapped into screen coords (for the playhead fallback). */
602
+ systems: Box[];
603
+ /** Per-(measure,staff) boxes mapped into screen coords (for highlights/cursor). */
604
+ measures: StaffMeasureBox[];
605
+ }
606
+ interface NotationLayoutOpts {
607
+ /** 0 = zoomed on bar 1, 1 = full excerpt (opening camera). Default 1. */
608
+ zoom01?: number;
609
+ /** Follow window (canvas coords); when set, overrides zoom and scrolls. */
610
+ focusBox?: Box | null;
611
+ }
612
+ /**
613
+ * Compute the notation layout (src crop + dest rect + mapped boxes) for one
614
+ * frame. This is RSR's `drawNotation` with the single `ctx.drawImage` call REMOVED
615
+ * — pure geometry, so it is testable headless and shared by the notation +
616
+ * scroll-cursor layers. The layer does the drawImage using `src` + `rect`.
617
+ */
618
+ declare function notationLayout(rn: RenderedNotation, W: number, H: number, boxTop: number, boxH: number, opts?: NotationLayoutOpts): NotationLayout;
619
+ /** Per-measure grand-staff column boxes (both staves unioned) from a frame's
620
+ * mapped measures, in render order. (RSR measureColumns) */
621
+ declare function measureColumnsFromLayout(measures: StaffMeasureBox[]): MeasureColumnBox[];
622
+ /** The playhead line + alpha for progress `t01`. null when fully faded. (RSR
623
+ * drawPlayhead, with the actual stroke factored out into the layer.) */
624
+ interface PlayheadLine {
625
+ x: number;
626
+ y0: number;
627
+ y1: number;
628
+ alpha: number;
629
+ }
630
+ declare function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null;
631
+
632
+ /** Map a canvas-space box through a base notation layout into world/screen coords. */
633
+ declare function mapBoxThroughLayout(base: NotationLayout, b: Box): Box;
634
+ /** RSR's per-frame `src` crop from a follow box (FOLLOW_PAD expand + clamp) —
635
+ * the exact canvas window RSR's drawNotation blits (+page.svelte:289-298). */
636
+ declare function followSrcBox(rn: RenderedNotation, focusBoxCanvas: Box): Box;
637
+ /**
638
+ * The camera pose that scrolls/zooms the base (fixed) notation blit so the follow
639
+ * window fills a `viewW`×`viewH` viewport — the camera-primitive expression of
640
+ * RSR's per-frame re-crop. Composing this pose with the base blit reproduces RSR's
641
+ * `notationLayout({focusBox})` exactly (proven in scene-notation-camera.test.ts).
642
+ *
643
+ * We frame the SAME canvas window RSR crops (FOLLOW_PAD-expanded `src`), mapped
644
+ * into base-world coords, into the viewport. `viewW`/`viewH` are the dest-rect
645
+ * dimensions (so the contain-fit matches RSR's band-fit).
646
+ */
647
+ declare function cameraForFollow(rn: RenderedNotation, base: NotationLayout, focusBoxCanvas: Box, viewW: number, viewH: number): CameraState;
648
+
649
+ interface NotationEngraving {
650
+ rendered: RenderedNotation;
651
+ /** The base world layout (full content fitted to the band; zoom01=1). */
652
+ base: NotationLayout;
653
+ /** Notation band rect (screen px) the follow window fits into, per frame. */
654
+ bandTop: number;
655
+ bandHeight: number;
656
+ /**
657
+ * A pure follow-layout provider, published by scroll-cursor at init. notation
658
+ * calls it each frame to blit the SAME followed-bars window the cursor sweeps —
659
+ * so the two agree by construction AND z-order is correct (notation drawn first,
660
+ * cursor line on top), regardless of which layer the runner draws first.
661
+ * Absent when no scroll-cursor is in the scene (notation-only) -> base layout.
662
+ */
663
+ followLayoutAt?: (ctx: RenderCtx, tMs: number) => NotationLayout;
664
+ }
665
+ declare function setNotationEngraving(ctx: RenderCtx, eng: NotationEngraving): void;
666
+ declare function getNotationEngraving(ctx: RenderCtx): NotationEngraving | undefined;
667
+ /** scroll-cursor publishes its follow-layout provider; notation reads it. */
668
+ declare function setFollowLayoutProvider(ctx: RenderCtx, fn: (ctx: RenderCtx, tMs: number) => NotationLayout): void;
669
+
670
+ declare function isBlackKey(midi: number): boolean;
671
+ /** A resolved keyboard layout: which keys, where, pinned to a world strip. */
672
+ interface KeyboardLayout {
673
+ /** Lowest MIDI note drawn (inclusive). */
674
+ lowMidi: number;
675
+ /** Highest MIDI note drawn (inclusive). */
676
+ highMidi: number;
677
+ /** Strip left edge (world px). */
678
+ x: number;
679
+ /** Strip width (world px) — the white-key span. */
680
+ w: number;
681
+ /** Strip top edge (world px). */
682
+ top: number;
683
+ /** Strip height (world px) = white-key height. */
684
+ height: number;
685
+ /** White-key width (world px). */
686
+ whiteW: number;
687
+ /** First (leftmost) white key index, used to offset the white grid to x. */
688
+ firstWhiteIndex: number;
689
+ /** Count of white keys in [lowMidi, highMidi]. */
690
+ whiteCount: number;
691
+ }
692
+ /** Standard 88-key piano: A0 (21) .. C8 (108). */
693
+ declare const PIANO_LOW = 21;
694
+ declare const PIANO_HIGH = 108;
695
+ interface KeyboardLayoutOpts {
696
+ /** "88" = full piano; "auto" = derive from a pitch span (padded to whites). */
697
+ range?: '88' | 'auto';
698
+ /** For "auto": the [lowMidi, highMidi] span to cover (snapped out to whites,
699
+ * padded by one white each side so edge keys aren't flush). */
700
+ span?: [number, number];
701
+ /** Strip left/width/top/height in world px. */
702
+ x: number;
703
+ w: number;
704
+ top: number;
705
+ height: number;
706
+ }
707
+ /** Build a KeyboardLayout. Pure. */
708
+ declare function keyboardLayout(opts: KeyboardLayoutOpts): KeyboardLayout;
709
+ /** The pitch span [min,max] of a Score's notes (for "auto" range). */
710
+ declare function scorePitchSpan(midis: number[]): [number, number];
711
+ /**
712
+ * Resolve a KeyboardLayout from the shared keyboard placement props + the Score's
713
+ * pitch span. The `keyboard` and `falling-notes` layers BOTH call this with the
714
+ * same inputs in standalone mode, so they produce identical geometry even without
715
+ * the store (the store just covers props that differ). Pure.
716
+ *
717
+ * Placement: the strip spans the safe box horizontally; its bottom sits at
718
+ * `bottomY` (world px) with the given `height`.
719
+ */
720
+ declare function resolveKeyboardLayout(args: {
721
+ range: '88' | 'auto';
722
+ pitchMidis: number[];
723
+ left: number;
724
+ width: number;
725
+ bottomY: number;
726
+ height: number;
727
+ }): KeyboardLayout;
728
+ /**
729
+ * Center x (world px) of a note's KEY on the keyboard. White keys sit on the
730
+ * even grid; black keys are nudged to sit between their two neighbouring whites
731
+ * (the canonical piano offset), so a falling block lands centered over the right
732
+ * key. This is the SINGLE source both layers use — they agree by construction.
733
+ */
734
+ declare function keyCenterX(layout: KeyboardLayout, midi: number): number;
735
+ /** Width (world px) a falling-note column should use for a pitch. Black keys are
736
+ * drawn narrower (like the physical key); white keys ~ a white-key width. */
737
+ declare function keyColumnWidth(layout: KeyboardLayout, midi: number): number;
738
+ /** The drawable rectangle for a key on the keyboard strip (white or black). */
739
+ interface KeyRect {
740
+ x: number;
741
+ y: number;
742
+ w: number;
743
+ h: number;
744
+ black: boolean;
745
+ }
746
+ declare function keyRect(layout: KeyboardLayout, midi: number): KeyRect;
747
+ /** Whether `midi` is within the layout's drawn range. */
748
+ declare function inRange(layout: KeyboardLayout, midi: number): boolean;
749
+ /** All white-key midis in the layout (low..high), for drawing the bed. */
750
+ declare function whiteKeys(layout: KeyboardLayout): number[];
751
+ /** All black-key midis in the layout (low..high), drawn on top of the whites. */
752
+ declare function blackKeys(layout: KeyboardLayout): number[];
753
+ type ColorBy = 'hand' | 'pitch-class';
754
+ /** Right/left hand colours. R = theme accent (the lead colour); L = a cooler
755
+ * contrast. Both passed in so a host theme can override. */
756
+ interface HandColors {
757
+ R: string;
758
+ L: string;
759
+ }
760
+ /**
761
+ * Colour for a note. `hand` reads the Score's accurate hand (NOT a pitch
762
+ * threshold guess); `pitch-class` colours by chroma. Pure.
763
+ */
764
+ declare function noteColor(midi: number, hand: 'L' | 'R', colorBy: ColorBy, hands: HandColors): string;
765
+
766
+ interface KeyboardProps {
767
+ /** "88" = full piano; "auto" = fit the Score's pitch span. Default "auto". */
768
+ range?: '88' | 'auto';
769
+ /** Colour active keys by performing hand (Score-accurate) or by pitch class.
770
+ * Default "hand". */
771
+ colorBy?: ColorBy;
772
+ /** Strip height in world px. Default 220. */
773
+ height?: number;
774
+ /** Strip bottom edge in world px. Default safeBox.bottom. */
775
+ bottomY?: number;
776
+ /** Right/left hand colours. Defaults: R = theme.accent, L = theme.gold. */
777
+ handColors?: HandColors;
778
+ }
779
+ declare const keyboardFactory: LayerFactory<KeyboardProps>;
780
+
781
+ interface FallingNotesProps {
782
+ /** Pair with a `keyboard` layer (read its layout + hit-line). When false the
783
+ * layer is standalone and builds its own layout from the props below.
784
+ * Default true. */
785
+ keyboard?: boolean;
786
+ /** Colour by performing hand (Score-accurate) or by pitch class. Default "hand". */
787
+ colorBy?: ColorBy;
788
+ /** Lead time in ms a note is visible before landing. Default 2000. */
789
+ leadMs?: number;
790
+ /** Fall speed in px/sec. Alternative to leadMs (ignored when leadMs is given). */
791
+ speed?: number;
792
+ /** Glow flash at the hit-line while a note sounds. Default true. */
793
+ hitGlow?: boolean;
794
+ /** Standalone-only: keyboard range when `keyboard:false`. Default "auto". */
795
+ range?: '88' | 'auto';
796
+ /** Standalone-only: hit-line (world y). Default safeBox.bottom - 220. */
797
+ hitLineY?: number;
798
+ /** Top of the fall region (world y) — notes appear here. Default safeBox.top. */
799
+ topY?: number;
800
+ /** Right/left hand colours. Defaults: R = theme.accent, L = theme.gold. */
801
+ handColors?: HandColors;
802
+ }
803
+ declare const fallingNotesFactory: LayerFactory<FallingNotesProps>;
804
+
805
+ declare function setKeyboardLayout(ctx: RenderCtx, layout: KeyboardLayout): void;
806
+ declare function getKeyboardLayout(ctx: RenderCtx): KeyboardLayout | undefined;
807
+
808
+ interface FallingKeyboardDemoOpts {
809
+ /** Frame size. Default phone-portrait 1080×1920. */
810
+ size?: [number, number];
811
+ /** Theme key for the spec. Default "rsr". */
812
+ theme?: string;
813
+ /** falling-notes lead time (ms). Default 2200. */
814
+ leadMs?: number;
815
+ /** Colour mode for both layers. Default "hand". */
816
+ colorBy?: 'hand' | 'pitch-class';
817
+ /** Keyboard range. Default "auto" (fit the piece's pitch span). */
818
+ range?: '88' | 'auto';
819
+ /** Forwarded to scoreFromMusicXML (tempo fallback/override/osmdFactory). */
820
+ scoreOpts?: ScoreFromMusicXMLOpts;
821
+ }
822
+ /** The SceneSpec for the demo (background + falling-notes + keyboard, full clip). */
823
+ declare function fallingKeyboardDemoSpec(opts?: FallingKeyboardDemoOpts): SceneSpec;
824
+ /** Parse the demo Score from MusicXML (the caller supplies the XML string so the
825
+ * bundling/fixture path stays in the host/test, not in src). */
826
+ declare function fallingKeyboardDemoScore(xml: string, opts?: FallingKeyboardDemoOpts): Promise<Score>;
827
+
828
+ /** Common window props shared by every card wrapper. */
829
+ interface WindowProps {
830
+ /** Absolute segment start (ms) — t01 is measured from here. Default 0. */
831
+ startMs?: number;
832
+ /** Segment length (ms) over which t01 sweeps 0..1. Default = scene's natural duration. */
833
+ durationMs?: number;
834
+ }
835
+ interface HookProps extends WindowProps {
836
+ /** Hook lines (wraps HookSceneOpts.lines). */
837
+ lines: string[];
838
+ /** Optional brand override (wraps HookSceneOpts.brand). */
839
+ brand?: string;
840
+ }
841
+ declare const hookFactory: LayerFactory<HookProps>;
842
+ interface RevealProps extends WindowProps {
843
+ /** Title (wraps RevealSceneOpts.title). */
844
+ title: string;
845
+ /** Subtitle (wraps RevealSceneOpts.subtitle). */
846
+ subtitle: string;
847
+ /** Optional initials override (wraps RevealSceneOpts.initials). */
848
+ initials?: string;
849
+ /** Optional fun fact (wraps RevealSceneOpts.funFact). */
850
+ funFact?: string;
851
+ }
852
+ declare const revealFactory: LayerFactory<RevealProps>;
853
+ interface CtaProps extends WindowProps {
854
+ /** End-card lines (wraps CtaSceneOpts.lines): [headline, ...accent lines]. */
855
+ lines: string[];
856
+ }
857
+ declare const ctaFactory: LayerFactory<CtaProps>;
858
+ interface PortraitProps extends WindowProps {
859
+ /** Composer name → title + initials badge fallback (wraps RevealSceneOpts.title). */
860
+ title: string;
861
+ /** Subtitle line (e.g. dates / era). */
862
+ subtitle: string;
863
+ /** Portrait image URL (Wikimedia etc.); loaded crossOrigin in init. */
864
+ url?: string | null;
865
+ /** Optional initials override. */
866
+ initials?: string;
867
+ /** Optional fun fact (corpus fun_fact). */
868
+ funFact?: string;
869
+ }
870
+ declare const portraitFactory: LayerFactory<PortraitProps>;
871
+
872
+ /** Host-provided level source attached to RenderCtx by the app/capture harness. */
873
+ interface SpectrumInput {
874
+ /** Normalized 0..1 magnitudes for `bands` bars at time tMs (preferred). */
875
+ levels?(tMs: number, bands: number): Float32Array | number[] | null | undefined;
876
+ /** Raw byte-FFT (0..255), log-binned by the layer (AnalyserNode shape). */
877
+ byteFreq?(tMs: number): Uint8Array | null | undefined;
878
+ }
879
+ /** Augment RenderCtx with the optional spectrum input (declaration merging). */
880
+ declare module '../layer' {
881
+ interface RenderCtx {
882
+ /** Optional audio-reactive level source for the spectrum layer (host-wired). */
883
+ spectrum?: SpectrumInput;
884
+ }
885
+ }
886
+ interface SpectrumProps {
887
+ /** Number of bars. Default 44 (whozart SPEC_BARS). */
888
+ bars?: number;
889
+ /** Vertical center as a fraction of H. Default 0.46 (whozart). */
890
+ centerFrac?: number;
891
+ /** Max half-height as a fraction of H. Default 0.135 (whozart). */
892
+ maxHeightFrac?: number;
893
+ /** Bar fill at low magnitude (wine). Default theme.accent. */
894
+ colorLow?: string;
895
+ /** Bar fill at high magnitude (gold). Default theme.gold. */
896
+ colorHigh?: string;
897
+ /** Per-frame level provider (overrides ctx.spectrum). Pure fn of t for tests. */
898
+ levelsFn?: (tMs: number, bands: number) => Float32Array | number[] | null;
899
+ }
900
+ declare const spectrumFactory: LayerFactory<SpectrumProps>;
901
+
902
+ interface BrandingProps {
903
+ /** Wordmark to draw. Default theme.brand. */
904
+ logo?: string;
905
+ /** Show the safe-zone debug overlay on top (apps' ?safe=1). Default false. */
906
+ safezone?: boolean;
907
+ /** Vertical anchor as a fraction of safeBox height from its TOP. Default 1
908
+ * (bottom edge of the safe box). */
909
+ yFrac?: number;
910
+ /** Font size in px. Default 34. */
911
+ size?: number;
912
+ /** Text colour. Default theme.sepia. */
913
+ color?: string;
914
+ }
915
+ declare const brandingFactory: LayerFactory<BrandingProps>;
916
+ type SafeGuidesProps = Record<string, never>;
917
+ declare const safeGuidesFactory: LayerFactory<SafeGuidesProps>;
918
+
919
+ interface PromoCardsDemoOpts {
920
+ /** Frame size. Default phone-portrait 1080×1920. */
921
+ size?: [number, number];
922
+ /** Theme key. Default "rsr". */
923
+ theme?: string;
924
+ /** Total clip length (s). Default 12. */
925
+ totalSec?: number;
926
+ /** Hook segment length (s). Default 2.2. */
927
+ hookSec?: number;
928
+ /** Reveal/portrait segment length (s). Default 4. */
929
+ revealSec?: number;
930
+ /** CTA segment length (s) at the end. Default 3. */
931
+ ctaSec?: number;
932
+ /** Hook lines. */
933
+ hookLines?: string[];
934
+ /** Reveal/portrait title (composer). */
935
+ title?: string;
936
+ /** Reveal/portrait subtitle. */
937
+ subtitle?: string;
938
+ /** Optional portrait URL (loaded crossOrigin in init; null → initials badge). */
939
+ portraitUrl?: string | null;
940
+ /** Optional fun fact under the portrait. */
941
+ funFact?: string;
942
+ /** CTA lines. */
943
+ ctaLines?: string[];
944
+ /** Brand wordmark. Default theme.brand (resolved by the host). */
945
+ brand?: string;
946
+ }
947
+ /** Build the demo SceneSpec wiring the S4 wrapper layers across a timeline. */
948
+ declare function promoCardsDemoSpec(opts?: PromoCardsDemoOpts): SceneSpec;
949
+
950
+ export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BrandingProps, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ClickTrackOpts, type ColorBy, type CountInOpts, type CtaProps, type DroneOpts, type DuckWindow, type Easing, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type GateError, type GateInput, type HandColors, type HighlightRegion, type HookProps, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type Layer, type LayerFactory, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type Placement, type PlayheadLine, type PortraitProps, type PromoCardsDemoOpts, type RecordSceneSpecOpts, type Rect, type RenderCtx, type ResolvedSegment, type RevealProps, type SafeGuidesProps, type SceneSpec, type Score, type ScoreFromMusicXMLOpts, type ScoreNote, type ScrollCursorProps, type SpecLayer, type SpectrumInput, type SpectrumProps, type TempoMap, type TimeAnchor, type TimelineSegment, activeCue, applySchedule, applyToContext, assertGate, blackKeys, brandingFactory, buildScene, cameraForFollow, cameraTransform, clamp, clickTrackSchedule, countInLeadSec, countInSchedule, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, frameRect, getKeyboardLayout, getLayerFactory, getNotationEngraving, highlightIntensity, hookFactory, identityCamera, inRange, invLerp, isBlackKey, kenBurns, keyCenterX, keyColumnWidth, keyRect, keyboardFactory, keyboardLayout, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, measureColumnsFromLayout, measureCount, measureSpanBox, notationFactory, notationLayout, noteColor, noteSetXRange, playheadLine, portraitFactory, promoCardsDemoSpec, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, runGate, safeGuidesFactory, scoreFromMusicXML, scorePitchSpan, scrollCursorFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, setupHeadlessDom, spectrumFactory, visualTimelineMs, whiteKeys, worldToViewport };