@real-music-packages/web-core 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio.js +4 -4
- package/dist/index.js +5 -5
- package/dist/playback.d.ts +61 -0
- package/dist/playback.js +128 -0
- package/dist/playback.js.map +1 -0
- package/dist/scene/index.d.ts +14 -198
- package/dist/scene/index.js +18 -3
- package/dist/scene/index.js.map +1 -1
- package/dist/waveform-DdSMAbYQ.d.ts +202 -0
- package/package.json +5 -1
package/dist/audio.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
KEYS_PREFER_FLATS
|
|
3
3
|
} from "./chunk-BYZBLH25.js";
|
|
4
|
+
import {
|
|
5
|
+
getMidiNote,
|
|
6
|
+
midiToNoteName
|
|
7
|
+
} from "./chunk-GORQ5YMR.js";
|
|
4
8
|
import {
|
|
5
9
|
SALAMANDER_CDN_BASE,
|
|
6
10
|
SALAMANDER_URLS_8,
|
|
@@ -8,10 +12,6 @@ import {
|
|
|
8
12
|
createSalamanderSampler,
|
|
9
13
|
generateReverb
|
|
10
14
|
} from "./chunk-JVGAABTK.js";
|
|
11
|
-
import {
|
|
12
|
-
getMidiNote,
|
|
13
|
-
midiToNoteName
|
|
14
|
-
} from "./chunk-GORQ5YMR.js";
|
|
15
15
|
|
|
16
16
|
// src/engine.ts
|
|
17
17
|
var browser = typeof window !== "undefined";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
KEYS_PREFER_FLATS,
|
|
3
|
-
useFlatsForKeyFifths,
|
|
4
|
-
useFlatsForKeyName
|
|
5
|
-
} from "./chunk-BYZBLH25.js";
|
|
6
1
|
import {
|
|
7
2
|
INTERVALS,
|
|
8
3
|
MAX_FRET,
|
|
@@ -17,6 +12,11 @@ import {
|
|
|
17
12
|
midiToPositions,
|
|
18
13
|
noteToFret
|
|
19
14
|
} from "./chunk-ILIXNGPE.js";
|
|
15
|
+
import {
|
|
16
|
+
KEYS_PREFER_FLATS,
|
|
17
|
+
useFlatsForKeyFifths,
|
|
18
|
+
useFlatsForKeyName
|
|
19
|
+
} from "./chunk-BYZBLH25.js";
|
|
20
20
|
import {
|
|
21
21
|
ALL_KEYS,
|
|
22
22
|
MAJOR_SCALE_INTERVALS,
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { S as Score } from './waveform-DdSMAbYQ.js';
|
|
2
|
+
import './video.js';
|
|
3
|
+
|
|
4
|
+
/** createScorePlayer — play a `Score` (from `scoreFromMusicXML`) through a sink.
|
|
5
|
+
*
|
|
6
|
+
* The standard Web Audio lookahead pattern: a coarse `setTimeout` tick fires
|
|
7
|
+
* every ~100ms and schedules every note whose onset falls inside the next
|
|
8
|
+
* ~300ms at its *exact* time on the sink's audio clock. Timer jitter moves the
|
|
9
|
+
* tick, never the note — the audio clock is the only clock the ear hears.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately Tone-free, like the rest of this package's audio layer: the sink
|
|
12
|
+
* owns the instrument (a Tone.Sampler, an oscillator, a test fake) and this
|
|
13
|
+
* module owns nothing but time. Position is score-time in ms; wall time maps to
|
|
14
|
+
* it through `rate`, and every rate change or seek re-anchors the mapping so
|
|
15
|
+
* position is continuous.
|
|
16
|
+
*
|
|
17
|
+
* Seeking is onset-faithful, not state-faithful: a note whose onset is behind
|
|
18
|
+
* the new position does not sound, even if it would still be ringing there.
|
|
19
|
+
* Mid-note pickup needs per-note release tracking for a payoff nobody asked
|
|
20
|
+
* for yet; the seam is `scheduleWindow`.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
interface PlaybackSink {
|
|
24
|
+
/** Sound `midi` for `durSec` starting at `atAudioTime` on the sink's clock. */
|
|
25
|
+
noteAt(midi: number, durSec: number, atAudioTime: number): void;
|
|
26
|
+
/** The sink's audio clock, in seconds (AudioContext.currentTime). */
|
|
27
|
+
now(): number;
|
|
28
|
+
/** Silence everything — pause, seek, teardown. */
|
|
29
|
+
releaseAll(): void;
|
|
30
|
+
}
|
|
31
|
+
interface ScorePlayerOpts {
|
|
32
|
+
/** Playback rate; 1 plays the score's own tempo. */
|
|
33
|
+
rate?: number;
|
|
34
|
+
/** Scheduler granularity — how often the tick fires. */
|
|
35
|
+
tickMs?: number;
|
|
36
|
+
/** How far ahead each tick schedules. Must exceed tickMs comfortably. */
|
|
37
|
+
lookaheadMs?: number;
|
|
38
|
+
}
|
|
39
|
+
interface ScorePlayer {
|
|
40
|
+
play(): void;
|
|
41
|
+
pause(): void;
|
|
42
|
+
/** Jump to a position in score-time. Keeps playing/paused state. */
|
|
43
|
+
seekMs(ms: number): void;
|
|
44
|
+
setRate(rate: number): void;
|
|
45
|
+
loop: boolean;
|
|
46
|
+
/** ~Every tick while playing: the current position in score-ms. */
|
|
47
|
+
onTick?: (positionMs: number) => void;
|
|
48
|
+
/** The piece ran out (never fires when looping). */
|
|
49
|
+
onEnd?: () => void;
|
|
50
|
+
readonly positionMs: number;
|
|
51
|
+
readonly playing: boolean;
|
|
52
|
+
readonly rate: number;
|
|
53
|
+
}
|
|
54
|
+
declare function createScorePlayer(score: Score, sink: PlaybackSink, opts?: ScorePlayerOpts): ScorePlayer;
|
|
55
|
+
/** Per printed bar: where it starts in score-ms. For bar-wise scrub and cursor. */
|
|
56
|
+
declare function measureStartsMs(score: Score): Array<{
|
|
57
|
+
measure: number;
|
|
58
|
+
atMs: number;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
export { type PlaybackSink, type ScorePlayer, type ScorePlayerOpts, createScorePlayer, measureStartsMs };
|
package/dist/playback.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/playback.ts
|
|
2
|
+
var DEFAULT_TICK_MS = 100;
|
|
3
|
+
var DEFAULT_LOOKAHEAD_MS = 300;
|
|
4
|
+
function createScorePlayer(score, sink, opts = {}) {
|
|
5
|
+
const tickMs = opts.tickMs ?? DEFAULT_TICK_MS;
|
|
6
|
+
const lookaheadMs = opts.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS;
|
|
7
|
+
const notes = [...score.notes].sort((a, b) => a.onsetMs - b.onsetMs);
|
|
8
|
+
let rate = opts.rate ?? 1;
|
|
9
|
+
let playing = false;
|
|
10
|
+
let loop = false;
|
|
11
|
+
let anchorScoreMs = 0;
|
|
12
|
+
let anchorAudioSec = 0;
|
|
13
|
+
let restingMs = 0;
|
|
14
|
+
let nextIndex = 0;
|
|
15
|
+
let timer = null;
|
|
16
|
+
const positionNow = () => playing ? anchorScoreMs + (sink.now() - anchorAudioSec) * 1e3 * rate : restingMs;
|
|
17
|
+
function indexAt(scoreMs) {
|
|
18
|
+
let lo = 0, hi = notes.length;
|
|
19
|
+
while (lo < hi) {
|
|
20
|
+
const mid = lo + hi >> 1;
|
|
21
|
+
if (notes[mid].onsetMs < scoreMs) lo = mid + 1;
|
|
22
|
+
else hi = mid;
|
|
23
|
+
}
|
|
24
|
+
return lo;
|
|
25
|
+
}
|
|
26
|
+
function scheduleWindow() {
|
|
27
|
+
const pos = positionNow();
|
|
28
|
+
const horizon = pos + lookaheadMs * rate;
|
|
29
|
+
while (nextIndex < notes.length && notes[nextIndex].onsetMs < horizon) {
|
|
30
|
+
const n = notes[nextIndex++];
|
|
31
|
+
if (n.onsetMs < pos - 1) continue;
|
|
32
|
+
const at = anchorAudioSec + (n.onsetMs - anchorScoreMs) / 1e3 / rate;
|
|
33
|
+
sink.noteAt(n.pitchMidi, n.durMs / 1e3 / rate, at);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function tick() {
|
|
37
|
+
if (!playing) return;
|
|
38
|
+
scheduleWindow();
|
|
39
|
+
const pos = positionNow();
|
|
40
|
+
player.onTick?.(Math.min(pos, score.durationMs));
|
|
41
|
+
if (nextIndex >= notes.length && pos >= score.durationMs) {
|
|
42
|
+
if (loop) {
|
|
43
|
+
anchorScoreMs = 0;
|
|
44
|
+
anchorAudioSec = sink.now();
|
|
45
|
+
nextIndex = 0;
|
|
46
|
+
scheduleWindow();
|
|
47
|
+
} else {
|
|
48
|
+
stopClock(score.durationMs);
|
|
49
|
+
player.onEnd?.();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
timer = setTimeout(tick, tickMs);
|
|
54
|
+
}
|
|
55
|
+
function stopClock(atMs) {
|
|
56
|
+
restingMs = atMs;
|
|
57
|
+
playing = false;
|
|
58
|
+
if (timer) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
timer = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const player = {
|
|
64
|
+
play() {
|
|
65
|
+
if (playing) return;
|
|
66
|
+
if (restingMs >= score.durationMs) restingMs = 0;
|
|
67
|
+
playing = true;
|
|
68
|
+
anchorScoreMs = restingMs;
|
|
69
|
+
anchorAudioSec = sink.now();
|
|
70
|
+
nextIndex = indexAt(restingMs);
|
|
71
|
+
tick();
|
|
72
|
+
},
|
|
73
|
+
pause() {
|
|
74
|
+
if (!playing) return;
|
|
75
|
+
stopClock(positionNow());
|
|
76
|
+
sink.releaseAll();
|
|
77
|
+
},
|
|
78
|
+
seekMs(ms) {
|
|
79
|
+
const target = Math.max(0, Math.min(score.durationMs, ms));
|
|
80
|
+
if (playing) {
|
|
81
|
+
sink.releaseAll();
|
|
82
|
+
anchorScoreMs = target;
|
|
83
|
+
anchorAudioSec = sink.now();
|
|
84
|
+
nextIndex = indexAt(target);
|
|
85
|
+
} else {
|
|
86
|
+
restingMs = target;
|
|
87
|
+
nextIndex = indexAt(target);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
setRate(next) {
|
|
91
|
+
if (next <= 0 || next === rate) return;
|
|
92
|
+
if (playing) {
|
|
93
|
+
anchorScoreMs = positionNow();
|
|
94
|
+
anchorAudioSec = sink.now();
|
|
95
|
+
}
|
|
96
|
+
rate = next;
|
|
97
|
+
},
|
|
98
|
+
get loop() {
|
|
99
|
+
return loop;
|
|
100
|
+
},
|
|
101
|
+
set loop(v) {
|
|
102
|
+
loop = v;
|
|
103
|
+
},
|
|
104
|
+
get positionMs() {
|
|
105
|
+
return Math.min(positionNow(), score.durationMs);
|
|
106
|
+
},
|
|
107
|
+
get playing() {
|
|
108
|
+
return playing;
|
|
109
|
+
},
|
|
110
|
+
get rate() {
|
|
111
|
+
return rate;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
return player;
|
|
115
|
+
}
|
|
116
|
+
function measureStartsMs(score) {
|
|
117
|
+
const starts = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const n of score.notes) {
|
|
119
|
+
const seen = starts.get(n.measure);
|
|
120
|
+
if (seen === void 0 || n.onsetMs < seen) starts.set(n.measure, n.onsetMs);
|
|
121
|
+
}
|
|
122
|
+
return [...starts.entries()].map(([measure, atMs]) => ({ measure, atMs })).sort((a, b) => a.atMs - b.atMs);
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
createScorePlayer,
|
|
126
|
+
measureStartsMs
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=playback.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/playback.ts"],"sourcesContent":["/** createScorePlayer — play a `Score` (from `scoreFromMusicXML`) through a sink.\n *\n * The standard Web Audio lookahead pattern: a coarse `setTimeout` tick fires\n * every ~100ms and schedules every note whose onset falls inside the next\n * ~300ms at its *exact* time on the sink's audio clock. Timer jitter moves the\n * tick, never the note — the audio clock is the only clock the ear hears.\n *\n * Deliberately Tone-free, like the rest of this package's audio layer: the sink\n * owns the instrument (a Tone.Sampler, an oscillator, a test fake) and this\n * module owns nothing but time. Position is score-time in ms; wall time maps to\n * it through `rate`, and every rate change or seek re-anchors the mapping so\n * position is continuous.\n *\n * Seeking is onset-faithful, not state-faithful: a note whose onset is behind\n * the new position does not sound, even if it would still be ringing there.\n * Mid-note pickup needs per-note release tracking for a payoff nobody asked\n * for yet; the seam is `scheduleWindow`.\n */\n\nimport type { Score } from './scene/index';\n\nexport interface PlaybackSink {\n /** Sound `midi` for `durSec` starting at `atAudioTime` on the sink's clock. */\n noteAt(midi: number, durSec: number, atAudioTime: number): void;\n /** The sink's audio clock, in seconds (AudioContext.currentTime). */\n now(): number;\n /** Silence everything — pause, seek, teardown. */\n releaseAll(): void;\n}\n\nexport interface ScorePlayerOpts {\n /** Playback rate; 1 plays the score's own tempo. */\n rate?: number;\n /** Scheduler granularity — how often the tick fires. */\n tickMs?: number;\n /** How far ahead each tick schedules. Must exceed tickMs comfortably. */\n lookaheadMs?: number;\n}\n\nexport interface ScorePlayer {\n play(): void;\n pause(): void;\n /** Jump to a position in score-time. Keeps playing/paused state. */\n seekMs(ms: number): void;\n setRate(rate: number): void;\n loop: boolean;\n /** ~Every tick while playing: the current position in score-ms. */\n onTick?: (positionMs: number) => void;\n /** The piece ran out (never fires when looping). */\n onEnd?: () => void;\n readonly positionMs: number;\n readonly playing: boolean;\n readonly rate: number;\n}\n\nconst DEFAULT_TICK_MS = 100;\nconst DEFAULT_LOOKAHEAD_MS = 300;\n\nexport function createScorePlayer(\n score: Score,\n sink: PlaybackSink,\n opts: ScorePlayerOpts = {},\n): ScorePlayer {\n const tickMs = opts.tickMs ?? DEFAULT_TICK_MS;\n const lookaheadMs = opts.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS;\n // Sorted by onset once; the scheduler walks `nextIndex` forward and never\n // rescans, so a tick is O(notes due), not O(all notes).\n const notes = [...score.notes].sort((a, b) => a.onsetMs - b.onsetMs);\n\n let rate = opts.rate ?? 1;\n let playing = false;\n let loop = false;\n // The wall↔score mapping: score-ms = anchorScoreMs + (now - anchorAudioSec) * 1000 * rate.\n let anchorScoreMs = 0;\n let anchorAudioSec = 0;\n // Where the playhead rests while paused.\n let restingMs = 0;\n let nextIndex = 0;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const positionNow = (): number =>\n playing ? anchorScoreMs + (sink.now() - anchorAudioSec) * 1000 * rate : restingMs;\n\n function indexAt(scoreMs: number): number {\n let lo = 0, hi = notes.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (notes[mid].onsetMs < scoreMs) lo = mid + 1;\n else hi = mid;\n }\n return lo;\n }\n\n /** Schedule everything with an onset inside [position, position + lookahead). */\n function scheduleWindow() {\n const pos = positionNow();\n const horizon = pos + lookaheadMs * rate;\n while (nextIndex < notes.length && notes[nextIndex].onsetMs < horizon) {\n const n = notes[nextIndex++];\n if (n.onsetMs < pos - 1) continue; // behind the playhead (post-seek edge)\n const at = anchorAudioSec + (n.onsetMs - anchorScoreMs) / 1000 / rate;\n sink.noteAt(n.pitchMidi, n.durMs / 1000 / rate, at);\n }\n }\n\n function tick() {\n if (!playing) return;\n scheduleWindow();\n const pos = positionNow();\n player.onTick?.(Math.min(pos, score.durationMs));\n\n if (nextIndex >= notes.length && pos >= score.durationMs) {\n if (loop) {\n anchorScoreMs = 0;\n anchorAudioSec = sink.now();\n nextIndex = 0;\n scheduleWindow();\n } else {\n stopClock(score.durationMs);\n player.onEnd?.();\n return;\n }\n }\n timer = setTimeout(tick, tickMs);\n }\n\n function stopClock(atMs: number) {\n restingMs = atMs;\n playing = false;\n if (timer) { clearTimeout(timer); timer = null; }\n }\n\n const player: ScorePlayer = {\n play() {\n if (playing) return;\n if (restingMs >= score.durationMs) restingMs = 0; // play again from the top\n playing = true;\n anchorScoreMs = restingMs;\n anchorAudioSec = sink.now();\n nextIndex = indexAt(restingMs);\n tick();\n },\n\n pause() {\n if (!playing) return;\n stopClock(positionNow());\n sink.releaseAll();\n },\n\n seekMs(ms: number) {\n const target = Math.max(0, Math.min(score.durationMs, ms));\n if (playing) {\n sink.releaseAll();\n anchorScoreMs = target;\n anchorAudioSec = sink.now();\n nextIndex = indexAt(target);\n } else {\n restingMs = target;\n nextIndex = indexAt(target);\n }\n },\n\n setRate(next: number) {\n if (next <= 0 || next === rate) return;\n // Re-anchor at the current position so the playhead does not jump.\n if (playing) {\n anchorScoreMs = positionNow();\n anchorAudioSec = sink.now();\n }\n rate = next;\n },\n\n get loop() { return loop; },\n set loop(v: boolean) { loop = v; },\n get positionMs() { return Math.min(positionNow(), score.durationMs); },\n get playing() { return playing; },\n get rate() { return rate; },\n };\n\n return player;\n}\n\n/** Per printed bar: where it starts in score-ms. For bar-wise scrub and cursor. */\nexport function measureStartsMs(score: Score): Array<{ measure: number; atMs: number }> {\n const starts = new Map<number, number>();\n for (const n of score.notes) {\n const seen = starts.get(n.measure);\n if (seen === undefined || n.onsetMs < seen) starts.set(n.measure, n.onsetMs);\n }\n return [...starts.entries()]\n .map(([measure, atMs]) => ({ measure, atMs }))\n .sort((a, b) => a.atMs - b.atMs);\n}\n"],"mappings":"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","names":[]}
|
package/dist/scene/index.d.ts
CHANGED
|
@@ -1,145 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as AudioClock, S as Score, a as LayerFactory, R as RenderCtx, b as ScoreFromMusicXMLOpts, T as TempoMap, c as ScoreNote } from '../waveform-DdSMAbYQ.js';
|
|
2
|
+
export { L as Layer, d as SpectrumInput, e as SpectrumProps, W as WaveformInput, f as WaveformProps, s as scoreFromMusicXML, g as spectrumFactory, w as waveformFactory } from '../waveform-DdSMAbYQ.js';
|
|
3
|
+
import { PromoTheme, RecordOpts, Scene, SafeBox } from '../video.js';
|
|
2
4
|
import { RenderedNotation, Box, StaffMeasureBox, MeasureColumnBox } from '../promo.js';
|
|
3
5
|
|
|
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
|
-
* Audio clock the runner exposes to layers. `nowMs` is the current playback
|
|
74
|
-
* position in milliseconds (audio-clock-driven, NOT wall-clock). During an
|
|
75
|
-
* offline/deterministic render the runner supplies the frame time directly, so
|
|
76
|
-
* `nowMs` and the `tMs` passed to `draw` agree.
|
|
77
|
-
*/
|
|
78
|
-
interface AudioClock {
|
|
79
|
-
/** Current playback position in ms. */
|
|
80
|
-
nowMs(): number;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Everything a layer needs to draw a frame. Constructed once per render and
|
|
84
|
-
* passed to every `init`/`draw`. World coordinates: layers draw in the frame's
|
|
85
|
-
* pixel space (0,0 top-left, W×H); the camera primitive (./camera) applies any
|
|
86
|
-
* pan/zoom transform to `ctx2d` BEFORE the layer's `draw` runs, so a layer never
|
|
87
|
-
* re-derives the viewport.
|
|
88
|
-
*/
|
|
89
|
-
interface RenderCtx {
|
|
90
|
-
/** The shared 2D context all layers draw onto. */
|
|
91
|
-
ctx2d: CanvasRenderingContext2D;
|
|
92
|
-
/** Frame width in px. */
|
|
93
|
-
W: number;
|
|
94
|
-
/** Frame height in px. */
|
|
95
|
-
H: number;
|
|
96
|
-
/** The parsed Score (timing/pitch/hands/lyrics). May be undefined for
|
|
97
|
-
* non-musical scenes (a pure hook/CTA card). */
|
|
98
|
-
score?: Score;
|
|
99
|
-
/** Audio playback clock. */
|
|
100
|
-
audioClock: AudioClock;
|
|
101
|
-
/** Per-app theme tokens (colours/fonts/brand). */
|
|
102
|
-
theme: PromoTheme;
|
|
103
|
-
/** Phone-safe content rectangle (./video safeBox) — layers anchor to this
|
|
104
|
-
* instead of re-deriving insets. */
|
|
105
|
-
safeBox: SafeBox;
|
|
106
|
-
/** Capture frame rate. */
|
|
107
|
-
fps: number;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* A composable, time-synced render component.
|
|
111
|
-
*
|
|
112
|
-
* @typeParam P the layer's prop type (what a SceneSpec passes as `p`).
|
|
113
|
-
*/
|
|
114
|
-
interface Layer<P = unknown> {
|
|
115
|
-
/** Stable identity, e.g. "notation" | "falling-notes" | "caption". */
|
|
116
|
-
readonly key: string;
|
|
117
|
-
/**
|
|
118
|
-
* One-time setup: load assets, lay out, rasterize an offscreen bitmap, etc.
|
|
119
|
-
* Runs before the first captured frame. May be async (e.g. font/IR load).
|
|
120
|
-
*/
|
|
121
|
-
init(ctx: RenderCtx, props: P): void | Promise<void>;
|
|
122
|
-
/**
|
|
123
|
-
* Per-frame draw. MUST be cheap and a pure function of `tMs` (no rAF / wall
|
|
124
|
-
* clock). `tMs` is the absolute playback time in ms.
|
|
125
|
-
*/
|
|
126
|
-
draw(ctx: RenderCtx, tMs: number): void;
|
|
127
|
-
/** Optional teardown (free bitmaps / audio nodes). */
|
|
128
|
-
dispose?(): void;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* A layer factory keyed by name, with a runtime prop validator so the runner /
|
|
132
|
-
* pre-render gate can reject an unknown prop before capture (spec: "unknown
|
|
133
|
-
* layer/prop → fail fast"). `validateProps` returns an array of human-readable
|
|
134
|
-
* errors ([] = valid).
|
|
135
|
-
*/
|
|
136
|
-
interface LayerFactory<P = unknown> {
|
|
137
|
-
key: string;
|
|
138
|
-
create(): Layer<P>;
|
|
139
|
-
/** Validate a SceneSpec's `p` object. Return [] when valid. */
|
|
140
|
-
validateProps(props: unknown): string[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
6
|
/** One scheduled audio event, in seconds RELATIVE to the schedule start. */
|
|
144
7
|
interface AudioEvent {
|
|
145
8
|
/** Offset from schedule start, seconds. */
|
|
@@ -1130,36 +993,6 @@ interface PortraitProps extends WindowProps {
|
|
|
1130
993
|
}
|
|
1131
994
|
declare const portraitFactory: LayerFactory<PortraitProps>;
|
|
1132
995
|
|
|
1133
|
-
/** Host-provided level source attached to RenderCtx by the app/capture harness. */
|
|
1134
|
-
interface SpectrumInput {
|
|
1135
|
-
/** Normalized 0..1 magnitudes for `bands` bars at time tMs (preferred). */
|
|
1136
|
-
levels?(tMs: number, bands: number): Float32Array | number[] | null | undefined;
|
|
1137
|
-
/** Raw byte-FFT (0..255), log-binned by the layer (AnalyserNode shape). */
|
|
1138
|
-
byteFreq?(tMs: number): Uint8Array | null | undefined;
|
|
1139
|
-
}
|
|
1140
|
-
/** Augment RenderCtx with the optional spectrum input (declaration merging). */
|
|
1141
|
-
declare module '../layer' {
|
|
1142
|
-
interface RenderCtx {
|
|
1143
|
-
/** Optional audio-reactive level source for the spectrum layer (host-wired). */
|
|
1144
|
-
spectrum?: SpectrumInput;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
interface SpectrumProps {
|
|
1148
|
-
/** Number of bars. Default 44 (whozart SPEC_BARS). */
|
|
1149
|
-
bars?: number;
|
|
1150
|
-
/** Vertical center as a fraction of H. Default 0.46 (whozart). */
|
|
1151
|
-
centerFrac?: number;
|
|
1152
|
-
/** Max half-height as a fraction of H. Default 0.135 (whozart). */
|
|
1153
|
-
maxHeightFrac?: number;
|
|
1154
|
-
/** Bar fill at low magnitude (wine). Default theme.accent. */
|
|
1155
|
-
colorLow?: string;
|
|
1156
|
-
/** Bar fill at high magnitude (gold). Default theme.gold. */
|
|
1157
|
-
colorHigh?: string;
|
|
1158
|
-
/** Per-frame level provider (overrides ctx.spectrum). Pure fn of t for tests. */
|
|
1159
|
-
levelsFn?: (tMs: number, bands: number) => Float32Array | number[] | null;
|
|
1160
|
-
}
|
|
1161
|
-
declare const spectrumFactory: LayerFactory<SpectrumProps>;
|
|
1162
|
-
|
|
1163
996
|
interface BrandingProps {
|
|
1164
997
|
/** Wordmark to draw. Default theme.brand. */
|
|
1165
998
|
logo?: string;
|
|
@@ -1837,33 +1670,6 @@ interface CountdownProps {
|
|
|
1837
1670
|
}
|
|
1838
1671
|
declare const countdownFactory: LayerFactory<CountdownProps>;
|
|
1839
1672
|
|
|
1840
|
-
/** Host-provided time-domain sample source (AnalyserNode shape). */
|
|
1841
|
-
interface WaveformInput {
|
|
1842
|
-
/** 0..255 samples centered ~128 (AnalyserNode.getByteTimeDomainData). */
|
|
1843
|
-
byteTime?(tMs: number): Uint8Array | null | undefined;
|
|
1844
|
-
}
|
|
1845
|
-
declare module '../layer' {
|
|
1846
|
-
interface RenderCtx {
|
|
1847
|
-
/** Optional time-domain source for the waveform layer (host-wired). */
|
|
1848
|
-
waveform?: WaveformInput;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
interface WaveformProps {
|
|
1852
|
-
/** Number of samples plotted. Default 128. */
|
|
1853
|
-
samples?: number;
|
|
1854
|
-
/** Vertical center as a fraction of H. Default 0.46. */
|
|
1855
|
-
centerFrac?: number;
|
|
1856
|
-
/** Max deflection as a fraction of H. Default 0.10. */
|
|
1857
|
-
amplitudeFrac?: number;
|
|
1858
|
-
/** Line width px. Default 5. */
|
|
1859
|
-
lineWidth?: number;
|
|
1860
|
-
/** Line colour. Default theme.accent. */
|
|
1861
|
-
color?: string;
|
|
1862
|
-
/** Per-frame sample provider returning -1..1 (overrides ctx.waveform). */
|
|
1863
|
-
samplesFn?: (tMs: number, n: number) => Float32Array | number[] | null;
|
|
1864
|
-
}
|
|
1865
|
-
declare const waveformFactory: LayerFactory<WaveformProps>;
|
|
1866
|
-
|
|
1867
1673
|
/** Semitone interval sets (from the root) for the supported scales/modes. */
|
|
1868
1674
|
declare const SCALE_INTERVALS: Record<string, number[]>;
|
|
1869
1675
|
interface ScaleHighlightProps {
|
|
@@ -1990,6 +1796,14 @@ interface EndCardProps {
|
|
|
1990
1796
|
declare const endCardFactory: LayerFactory<EndCardProps>;
|
|
1991
1797
|
|
|
1992
1798
|
type ImageRevealMode = 'pixelate' | 'shuffle' | 'blur';
|
|
1799
|
+
/**
|
|
1800
|
+
* Reveal pacing curve applied to linear progress before it drives the effect.
|
|
1801
|
+
* - 'linear' : constant rate (default — unchanged behavior).
|
|
1802
|
+
* - 'easeOut' : fast at first, SLOWER near the end (quadratic).
|
|
1803
|
+
* - 'easeOutStrong' : like easeOut but a more DRAMATIC, drawn-out final reveal (cubic).
|
|
1804
|
+
* - 'easeInOut' : slow start (stays obscured), then a slow settle at the end.
|
|
1805
|
+
*/
|
|
1806
|
+
type RevealEasing = 'linear' | 'easeOut' | 'easeOutStrong' | 'easeInOut';
|
|
1993
1807
|
interface ImageRevealProps {
|
|
1994
1808
|
/** The image to reveal. Any CanvasImageSource (HTMLImageElement, OffscreenCanvas, …). */
|
|
1995
1809
|
image: CanvasImageSource;
|
|
@@ -2006,7 +1820,9 @@ interface ImageRevealProps {
|
|
|
2006
1820
|
* - blur: controls maximum blur radius in px (0→mild, 1→extreme; default 0.5 ≈ 40px).
|
|
2007
1821
|
*/
|
|
2008
1822
|
difficulty?: number;
|
|
1823
|
+
/** Pacing curve for the reveal. Default 'linear'. See RevealEasing. */
|
|
1824
|
+
easing?: RevealEasing;
|
|
2009
1825
|
}
|
|
2010
1826
|
declare const imageRevealFactory: LayerFactory<ImageRevealProps>;
|
|
2011
1827
|
|
|
2012
|
-
export { type Affine,
|
|
1828
|
+
export { type Affine, AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type FretboardProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type ImageRevealMode, type ImageRevealProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, LayerFactory, type McqCardProps, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type ParticleBurstProps, type PitchContourProps, type Placement, type PlayheadLine, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, type RadialSpectrumProps, type RayEndpoints, type RecordSceneSpecOpts, type Rect, RenderCtx, type ResolvedSegment, type RevealEasing, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, Score, ScoreFromMusicXMLOpts, ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type StaffKeyboardRayProps, type StatCounterProps, TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, audioPlayheadLine, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, degreeLabel, degreeLabelsFactory, distinctMeasureIndices, distinctOnsets, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, fracSlotPoint, frameRect, fretboardFactory, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, imageRevealFactory, inRange, intervalArcsFactory, invLerp, isBlackKey, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureColumnsFromLayout, measureCount, measureSpanBox, measureSpans, measureSystemMap, timeToX as minimapTimeToX, msPerBeat, notationFactory, notationLayout, noteColor, noteSetXRange, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pitchAt, pitchContourFactory, pitchRange, playheadLine, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, systemBox, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, vstackAudioPlayheadLine, vstackFollowBox, whiteKeys, worldToViewport };
|
package/dist/scene/index.js
CHANGED
|
@@ -44,6 +44,7 @@ async function scoreFromMusicXML(xml, opts = {}) {
|
|
|
44
44
|
while (!it.EndReached && steps++ < MAX_ITERATOR_STEPS) {
|
|
45
45
|
const enrolled = it.CurrentEnrolledTimestamp?.RealValue ?? 0;
|
|
46
46
|
const onsetMs = wholeNoteToMs(enrolled);
|
|
47
|
+
const measure = it.CurrentMeasure?.MeasureNumber ?? (it.CurrentMeasureIndex ?? 0) + 1;
|
|
47
48
|
const voiceEntries = it.CurrentAudibleVoiceEntries?.() ?? [];
|
|
48
49
|
for (const ve of voiceEntries) {
|
|
49
50
|
const voiceId = ve.ParentVoice?.VoiceId ?? 1;
|
|
@@ -69,7 +70,8 @@ async function scoreFromMusicXML(xml, opts = {}) {
|
|
|
69
70
|
durMs: Math.round(wholeNoteToMs(wholeNoteLen)),
|
|
70
71
|
staff: staffIdx,
|
|
71
72
|
voice: voiceId,
|
|
72
|
-
hand
|
|
73
|
+
hand,
|
|
74
|
+
measure
|
|
73
75
|
};
|
|
74
76
|
const lyric = extractLyric(ve);
|
|
75
77
|
if (lyric != null) note.lyric = lyric;
|
|
@@ -4766,11 +4768,18 @@ var endCardFactory = {
|
|
|
4766
4768
|
};
|
|
4767
4769
|
|
|
4768
4770
|
// src/scene/layers/imageReveal.ts
|
|
4771
|
+
function applyEasing(p, easing) {
|
|
4772
|
+
if (easing === "easeOut") return 1 - (1 - p) * (1 - p);
|
|
4773
|
+
if (easing === "easeOutStrong") return 1 - (1 - p) * (1 - p) * (1 - p);
|
|
4774
|
+
if (easing === "easeInOut") return p * p * (3 - 2 * p);
|
|
4775
|
+
return p;
|
|
4776
|
+
}
|
|
4769
4777
|
var DEFAULTS11 = {
|
|
4770
4778
|
mode: "pixelate",
|
|
4771
4779
|
startMs: 0,
|
|
4772
4780
|
durationMs: 6e3,
|
|
4773
|
-
difficulty: 0.5
|
|
4781
|
+
difficulty: 0.5,
|
|
4782
|
+
easing: "linear"
|
|
4774
4783
|
};
|
|
4775
4784
|
function clamp017(x) {
|
|
4776
4785
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
@@ -4799,6 +4808,7 @@ function imageRevealLayer() {
|
|
|
4799
4808
|
let startMs = DEFAULTS11.startMs;
|
|
4800
4809
|
let durationMs = DEFAULTS11.durationMs;
|
|
4801
4810
|
let difficulty = DEFAULTS11.difficulty;
|
|
4811
|
+
let easing = DEFAULTS11.easing;
|
|
4802
4812
|
let image = null;
|
|
4803
4813
|
let offscreen = null;
|
|
4804
4814
|
let offCtx = null;
|
|
@@ -4832,6 +4842,7 @@ function imageRevealLayer() {
|
|
|
4832
4842
|
startMs = props.startMs ?? DEFAULTS11.startMs;
|
|
4833
4843
|
durationMs = props.durationMs ?? DEFAULTS11.durationMs;
|
|
4834
4844
|
difficulty = props.difficulty ?? DEFAULTS11.difficulty;
|
|
4845
|
+
easing = props.easing ?? DEFAULTS11.easing;
|
|
4835
4846
|
image = props.image;
|
|
4836
4847
|
const src = props.image;
|
|
4837
4848
|
imgW = src["naturalWidth"] ?? src["displayWidth"] ?? src["width"] ?? 0;
|
|
@@ -4839,7 +4850,8 @@ function imageRevealLayer() {
|
|
|
4839
4850
|
},
|
|
4840
4851
|
draw(ctx, tMs) {
|
|
4841
4852
|
if (!image || imgW === 0 || imgH === 0) return;
|
|
4842
|
-
const
|
|
4853
|
+
const raw = clamp017((tMs - startMs) / (durationMs > 0 ? durationMs : 1));
|
|
4854
|
+
const p = applyEasing(raw, easing);
|
|
4843
4855
|
const sb = ctx.safeBox;
|
|
4844
4856
|
const fit = fitRect(imgW, imgH, sb.left, sb.top, sb.w, sb.h);
|
|
4845
4857
|
const c = ctx.ctx2d;
|
|
@@ -4951,6 +4963,7 @@ function drawShuffle(c, image, fit, p, difficulty, imgW, imgH) {
|
|
|
4951
4963
|
}
|
|
4952
4964
|
}
|
|
4953
4965
|
var VALID_MODES = ["pixelate", "shuffle", "blur"];
|
|
4966
|
+
var VALID_EASINGS = ["linear", "easeOut", "easeOutStrong", "easeInOut"];
|
|
4954
4967
|
var imageRevealFactory = {
|
|
4955
4968
|
key: "image-reveal",
|
|
4956
4969
|
create: imageRevealLayer,
|
|
@@ -4967,6 +4980,8 @@ var imageRevealFactory = {
|
|
|
4967
4980
|
errs.push("image-reveal.durationMs must be a positive number");
|
|
4968
4981
|
if (p["difficulty"] != null && (typeof p["difficulty"] !== "number" || p["difficulty"] < 0 || p["difficulty"] > 1))
|
|
4969
4982
|
errs.push("image-reveal.difficulty must be a number in [0, 1]");
|
|
4983
|
+
if (p["easing"] != null && !VALID_EASINGS.includes(p["easing"]))
|
|
4984
|
+
errs.push(`image-reveal.easing must be one of: ${VALID_EASINGS.join(", ")}`);
|
|
4970
4985
|
return errs;
|
|
4971
4986
|
}
|
|
4972
4987
|
};
|