@kieransimkin/dancemoves 3.2.0 → 3.3.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/README.md +16 -1
- package/docs/beat-alignment-media.json +99 -0
- package/docs/beat-alignment.md +79 -0
- package/docs/screenshots/beat-alignment-first-beat.png +0 -0
- package/docs/videos/beat-alignment-browser.mp4 +0 -0
- package/lib/admin.min.js +1 -1
- package/lib/build-manifest.json +21 -21
- package/lib/dancemoves.min.js +3 -3
- package/lib/index.cjs +100 -16
- package/lib/index.d.ts +13 -3
- package/lib/index.mjs +100 -16
- package/lib/react.cjs +7 -2
- package/lib/react.mjs +7 -2
- package/lib/server.cjs +54 -11
- package/lib/server.mjs +54 -11
- package/lib/wordpress.js +3 -3
- package/package.json +1 -1
- package/readme.txt +2 -2
package/lib/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
createPlaybackPublisher: () => createPlaybackPublisher,
|
|
28
28
|
createPlaybackReceiver: () => createPlaybackReceiver,
|
|
29
29
|
fromWordPressMeta: () => fromWordPressMeta,
|
|
30
|
+
validateBeatAlignment: () => validateBeatAlignment,
|
|
30
31
|
validatePageConfig: () => validatePageConfig
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -135,6 +136,38 @@ function install(__dmScope) {
|
|
|
135
136
|
return ticks;
|
|
136
137
|
}
|
|
137
138
|
var bpm = effectiveBpm(rawConfig.bpm);
|
|
139
|
+
var bpmSource = ["explicit", "analysis"].indexOf(rawConfig.bpmSource) >= 0 ? rawConfig.bpmSource : "fallback";
|
|
140
|
+
function beatAlignmentState(audio) {
|
|
141
|
+
var reading = rawConfig.beatAlignment;
|
|
142
|
+
if (!reading)
|
|
143
|
+
return { status: rawConfig.beatAlignmentStatus === "invalid" ? "invalid" : "unconfigured", applied: false };
|
|
144
|
+
var source = playbackAudio(audio);
|
|
145
|
+
var identity2 = source && (source.masterSourceUrl || source.masterId || source.currentSrc || source.src);
|
|
146
|
+
try {
|
|
147
|
+
if (!identity2 || new URL(String(identity2), document2.baseURI).href !== new URL(reading.source_url).href) {
|
|
148
|
+
return { status: "source-mismatch", applied: false };
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
return { status: "source-mismatch", applied: false };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
status: reading.phase_status,
|
|
155
|
+
applied: true,
|
|
156
|
+
firstBeatSeconds: reading.first_beat_seconds,
|
|
157
|
+
offsetPips: reading.offset_pips,
|
|
158
|
+
sourceSha256: reading.source_sha256,
|
|
159
|
+
bpmStatus: reading.bpm_status
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function rhythmicTime(audio) {
|
|
163
|
+
var source = playbackAudio(audio);
|
|
164
|
+
var seconds = source && Number.isFinite(Number(source.currentTime)) ? Number(source.currentTime) : 0;
|
|
165
|
+
var state = beatAlignmentState(source);
|
|
166
|
+
return seconds - (state.applied ? state.firstBeatSeconds : 0);
|
|
167
|
+
}
|
|
168
|
+
function sourceTick(audio, bpmOverride) {
|
|
169
|
+
return rhythmicTime(audio) * effectiveBpm(bpmOverride || bpm) * TICKS_PER_BEAT / 60;
|
|
170
|
+
}
|
|
138
171
|
function durationMilliseconds(ticks, bpmOverride) {
|
|
139
172
|
return 3750 / effectiveBpm(bpmOverride || bpm) * quantizeTicks(ticks);
|
|
140
173
|
}
|
|
@@ -142,7 +175,7 @@ function install(__dmScope) {
|
|
|
142
175
|
var settings = options || {};
|
|
143
176
|
var audio = playbackAudio(settings.audio) || (settings.clock === "audio" ? activeAudio : null);
|
|
144
177
|
if (audio && !audio.paused && !audio.ended && Number.isFinite(Number(audio.currentTime))) {
|
|
145
|
-
return
|
|
178
|
+
return sourceTick(audio, settings.bpm);
|
|
146
179
|
}
|
|
147
180
|
var now = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
|
|
148
181
|
return Math.max(0, now - pageClockStarted) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 6e4;
|
|
@@ -441,7 +474,7 @@ function install(__dmScope) {
|
|
|
441
474
|
__dmScope.dataset(document2.documentElement, "danceMovesSharedControls", null);
|
|
442
475
|
}
|
|
443
476
|
__dmScope.dataset(document2.documentElement, "danceMovesBpm", String(bpm));
|
|
444
|
-
__dmScope.dataset(document2.documentElement, "danceMovesBpmSource",
|
|
477
|
+
__dmScope.dataset(document2.documentElement, "danceMovesBpmSource", bpmSource);
|
|
445
478
|
__dmScope.dataset(document2.documentElement, "danceMovesVersion", String(rawConfig.version || ""));
|
|
446
479
|
}
|
|
447
480
|
function registerAnimationScope(root, selectors) {
|
|
@@ -977,7 +1010,7 @@ function install(__dmScope) {
|
|
|
977
1010
|
var root = document2.querySelector(".ks-epk");
|
|
978
1011
|
if (root) {
|
|
979
1012
|
__dmScope.dataset(root, "danceMovesBpm", String(bpm));
|
|
980
|
-
__dmScope.dataset(root, "danceMovesBpmSource",
|
|
1013
|
+
__dmScope.dataset(root, "danceMovesBpmSource", bpmSource);
|
|
981
1014
|
if (rawConfig.lyricTimingUrl)
|
|
982
1015
|
__dmScope.dataset(root, "danceMovesLyricTiming", String(rawConfig.lyricTimingUrl));
|
|
983
1016
|
if (rawConfig.cueTimingUrl)
|
|
@@ -1006,11 +1039,14 @@ function install(__dmScope) {
|
|
|
1006
1039
|
var api = {
|
|
1007
1040
|
version: String(rawConfig.version || ""),
|
|
1008
1041
|
bpm,
|
|
1009
|
-
bpmSource
|
|
1042
|
+
bpmSource,
|
|
1010
1043
|
ticksPerBeat: TICKS_PER_BEAT,
|
|
1011
1044
|
quantizeTicks,
|
|
1012
1045
|
durationMilliseconds,
|
|
1013
1046
|
currentTick,
|
|
1047
|
+
sourceTick,
|
|
1048
|
+
rhythmicTime,
|
|
1049
|
+
beatAlignmentState,
|
|
1014
1050
|
nextIntervalTick,
|
|
1015
1051
|
scheduleAtInterval,
|
|
1016
1052
|
deferStart,
|
|
@@ -1217,7 +1253,8 @@ function install2(__dmScope) {
|
|
|
1217
1253
|
function sync() {
|
|
1218
1254
|
var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
|
|
1219
1255
|
var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
|
|
1220
|
-
var
|
|
1256
|
+
var rhythmicSeconds = motion.rhythmicTime ? motion.rhythmicTime(audio) : finite3(audio.currentTime, 0);
|
|
1257
|
+
var phase = -((rhythmicSeconds / rate % duration + duration) % duration);
|
|
1221
1258
|
__dmScope.setStyle(state.root.style, prefix + "-duration", duration.toFixed(6) + "s");
|
|
1222
1259
|
__dmScope.setStyle(state.root.style, prefix + "-delay", phase.toFixed(6) + "s");
|
|
1223
1260
|
__dmScope.dataset(state.root, "danceMovesPulse", state.reduced.matches || state.forced.matches ? "inactive" : audio.paused ? "paused" : "playing");
|
|
@@ -1509,7 +1546,8 @@ function install2(__dmScope) {
|
|
|
1509
1546
|
function calculate(reason, staticFallback) {
|
|
1510
1547
|
var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
|
|
1511
1548
|
var time = Math.max(0, finite3(audio.currentTime, 0));
|
|
1512
|
-
var
|
|
1549
|
+
var rhythmicSeconds = motion.rhythmicTime ? motion.rhythmicTime(audio) : time;
|
|
1550
|
+
var phase = ((rhythmicSeconds - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
|
|
1513
1551
|
var localFrame = staticFallback ? staticFrame : Math.min(frameCount - 1, Math.floor(phase * frameCount + 1e-9));
|
|
1514
1552
|
var mapped = mappedFrame(localFrame);
|
|
1515
1553
|
return {
|
|
@@ -4928,8 +4966,9 @@ function install9(__dmScope) {
|
|
|
4928
4966
|
anchor = 0;
|
|
4929
4967
|
lastClock = mode;
|
|
4930
4968
|
const tempo = bpm();
|
|
4931
|
-
const
|
|
4932
|
-
|
|
4969
|
+
const sourceBeats = useAudio ? Math.max(0, Number.isFinite(audio.currentTime) ? audio.currentTime : 0) * tempo / 60 : motion.currentTick({ clock: "page" }) / 16;
|
|
4970
|
+
const beats = useAudio && motion.sourceTick ? motion.sourceTick(audio) / 16 : sourceBeats;
|
|
4971
|
+
return { beats, sourceBeats, mode, tempo };
|
|
4933
4972
|
}
|
|
4934
4973
|
function blocked() {
|
|
4935
4974
|
if (!enabled)
|
|
@@ -4967,7 +5006,8 @@ function install9(__dmScope) {
|
|
|
4967
5006
|
audio: s.mode === "audio" ? audio : null,
|
|
4968
5007
|
rudiment: row.name,
|
|
4969
5008
|
clock: s.mode,
|
|
4970
|
-
sourceBeats: s.
|
|
5009
|
+
sourceBeats: s.sourceBeats,
|
|
5010
|
+
rhythmicBeats: s.beats,
|
|
4971
5011
|
bpm: s.tempo,
|
|
4972
5012
|
rate,
|
|
4973
5013
|
pip: neutral ? null : pip,
|
|
@@ -5513,6 +5553,35 @@ function createScope(root, config, { legacyGlobals = false, nonce, orientation =
|
|
|
5513
5553
|
return api;
|
|
5514
5554
|
}
|
|
5515
5555
|
|
|
5556
|
+
// .build/modules/beat-alignment.mjs
|
|
5557
|
+
var hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
5558
|
+
function rational(value, label) {
|
|
5559
|
+
if (typeof value !== "string" || !/^-?\d{1,80}(?:\/[1-9]\d{0,79})?$/.test(value)) throw new TypeError(`Invalid exact ${label}`);
|
|
5560
|
+
const [n, d = "1"] = value.split("/");
|
|
5561
|
+
return [BigInt(n), BigInt(d)];
|
|
5562
|
+
}
|
|
5563
|
+
function numeric(value, label, min, max) {
|
|
5564
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`Invalid ${label}`);
|
|
5565
|
+
return value;
|
|
5566
|
+
}
|
|
5567
|
+
function validateBeatAlignment(input) {
|
|
5568
|
+
if (input === void 0 || input === null || input === "") return null;
|
|
5569
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || input.schema !== "stemlab.beat-alignment.v1" || input.pips_per_beat !== 64 || input.canonical_first_beat_pip !== 0 || !hash(input.source_sha256) || !hash(input.evidence_sha256) || !hash(input.report_sha256)) throw new TypeError("Invalid source-bound StemLab beat alignment");
|
|
5570
|
+
const bpm = numeric(input.bpm, "alignment BPM", 20, 400);
|
|
5571
|
+
const first = numeric(input.first_beat_seconds, "first beat", 0, 86400);
|
|
5572
|
+
const offset = numeric(input.offset_pips, "pip offset", -36864e3, 0);
|
|
5573
|
+
const [bn, bd] = rational(input.exact_bpm, "BPM"), [fn, fd] = rational(input.first_beat_exact_seconds, "first beat"), [pn, pd] = rational(input.exact_offset_pips, "pip offset");
|
|
5574
|
+
if (fn * bn * 64n * pd + pn * fd * bd * 60n !== 0n || bn <= 0n || fn < 0n || Math.abs(Number(bn) / Number(bd) - bpm) > 1e-9 || Math.abs(Number(fn) / Number(fd) - first) > 1e-9 || Math.abs(Number(pn) / Number(pd) - offset) > 1e-7) throw new RangeError("Exact beat alignment and numeric clock disagree");
|
|
5575
|
+
if (!["canonical", "estimated"].includes(input.bpm_status) || !["estimated", "confirmed", "synthetic"].includes(input.phase_status)) throw new TypeError("Alignment authority is required");
|
|
5576
|
+
for (const key of ["analysis_id", "bpm_basis"]) if (typeof input[key] !== "string" || !/^[a-zA-Z0-9_.-]{1,128}$/.test(input[key])) throw new TypeError(`Invalid ${key}`);
|
|
5577
|
+
if (input.source_audio_verified !== true || !input.source_audio || !Number.isSafeInteger(input.source_audio.num_frames) || input.source_audio.num_frames < 1 || !Number.isSafeInteger(input.source_audio.sample_rate) || input.source_audio.sample_rate < 1 || first >= input.source_audio.num_frames / input.source_audio.sample_rate) throw new TypeError("Verified source audio frame bounds are required");
|
|
5578
|
+
const sourceUrl = input.source_url;
|
|
5579
|
+
if (typeof sourceUrl !== "string" || sourceUrl.length > 4096 || /[\u0000-\u001f]/.test(sourceUrl)) throw new TypeError("An exact verified master URL is required");
|
|
5580
|
+
const parsed = new URL(sourceUrl);
|
|
5581
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) throw new TypeError("Invalid verified master URL");
|
|
5582
|
+
return Object.freeze({ ...input, bpm, first_beat_seconds: first, offset_pips: offset, source_audio: Object.freeze({ ...input.source_audio }) });
|
|
5583
|
+
}
|
|
5584
|
+
|
|
5516
5585
|
// .build/modules/config.mjs
|
|
5517
5586
|
var PAGE_META_KEYS = Object.freeze({
|
|
5518
5587
|
bpm: "_dance_moves_bpm",
|
|
@@ -5520,7 +5589,9 @@ var PAGE_META_KEYS = Object.freeze({
|
|
|
5520
5589
|
cue: "_dance_moves_cue_timing_id",
|
|
5521
5590
|
popups: "_dance_moves_lyric_popups_enabled",
|
|
5522
5591
|
duration: "_dance_moves_master_duration_ms",
|
|
5523
|
-
effect: "_dance_moves_effect"
|
|
5592
|
+
effect: "_dance_moves_effect",
|
|
5593
|
+
alignment: "_dance_moves_beat_alignment",
|
|
5594
|
+
alignmentAudio: "_dance_moves_beat_alignment_audio_id"
|
|
5524
5595
|
});
|
|
5525
5596
|
var object = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
5526
5597
|
function finite(value, name, min, max) {
|
|
@@ -5539,7 +5610,11 @@ function url(value, name) {
|
|
|
5539
5610
|
function validatePageConfig(input = {}) {
|
|
5540
5611
|
if (!object(input)) throw new TypeError("Page configuration must be an object");
|
|
5541
5612
|
const explicit = input.bpm !== void 0 && input.bpm !== null && input.bpm !== "";
|
|
5542
|
-
const
|
|
5613
|
+
const beatAlignment = validateBeatAlignment(input.beatAlignment);
|
|
5614
|
+
const canonicalExplicit = explicit && !["fallback", "analysis"].includes(input.bpmSource);
|
|
5615
|
+
if (canonicalExplicit && beatAlignment && Math.abs(input.bpm - beatAlignment.bpm) > 500001e-9) throw new RangeError("Stored BPM and selected beat alignment disagree; select a matching BPM basis");
|
|
5616
|
+
const bpm = beatAlignment ? beatAlignment.bpm : explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
|
|
5617
|
+
if (explicit) finite(input.bpm, "bpm", 20, 400);
|
|
5543
5618
|
const effect = input.effect ?? "";
|
|
5544
5619
|
if (!["", "paper-planes"].includes(effect)) throw new RangeError("effect must be empty or paper-planes");
|
|
5545
5620
|
if (input.lyricPopupsEnabled !== void 0 && typeof input.lyricPopupsEnabled !== "boolean") throw new TypeError("lyricPopupsEnabled must be boolean");
|
|
@@ -5551,8 +5626,9 @@ function validatePageConfig(input = {}) {
|
|
|
5551
5626
|
return Object.freeze({
|
|
5552
5627
|
pageId,
|
|
5553
5628
|
bpm,
|
|
5554
|
-
bpmSource:
|
|
5629
|
+
bpmSource: canonicalExplicit ? "explicit" : beatAlignment ? "analysis" : "fallback",
|
|
5555
5630
|
fallbackBpm: 120,
|
|
5631
|
+
beatAlignment,
|
|
5556
5632
|
ticksPerBeat: 16,
|
|
5557
5633
|
tickDefinition: "sixteenth-of-beat",
|
|
5558
5634
|
longDurationQuantumTicks: 16,
|
|
@@ -5568,9 +5644,9 @@ function validatePageConfig(input = {}) {
|
|
|
5568
5644
|
}
|
|
5569
5645
|
async function fromWordPressMeta(meta, resolveAttachment) {
|
|
5570
5646
|
if (!object(meta)) throw new TypeError("meta must be an object");
|
|
5571
|
-
const
|
|
5647
|
+
const numeric2 = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
|
|
5572
5648
|
const b = meta[PAGE_META_KEYS.bpm];
|
|
5573
|
-
const bpm =
|
|
5649
|
+
const bpm = numeric2(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
|
|
5574
5650
|
const resolve = async (key) => {
|
|
5575
5651
|
const value = Number(meta[key] || 0);
|
|
5576
5652
|
if (!Number.isSafeInteger(value)) throw new RangeError("Attachment ID must be a safe integer");
|
|
@@ -5580,8 +5656,15 @@ async function fromWordPressMeta(meta, resolveAttachment) {
|
|
|
5580
5656
|
return await resolveAttachment(id) || "";
|
|
5581
5657
|
};
|
|
5582
5658
|
const duration = Number(meta[PAGE_META_KEYS.duration]);
|
|
5659
|
+
let beatAlignment = meta[PAGE_META_KEYS.alignment] || null;
|
|
5660
|
+
if (typeof beatAlignment === "string") {
|
|
5661
|
+
if (beatAlignment.length > 16384) throw new RangeError("Beat alignment exceeds 16 KiB");
|
|
5662
|
+
beatAlignment = JSON.parse(beatAlignment);
|
|
5663
|
+
}
|
|
5664
|
+
if (beatAlignment) beatAlignment = { ...beatAlignment, source_url: await resolve(PAGE_META_KEYS.alignmentAudio) };
|
|
5583
5665
|
return validatePageConfig({
|
|
5584
5666
|
bpm,
|
|
5667
|
+
beatAlignment,
|
|
5585
5668
|
lyricTimingUrl: await resolve(PAGE_META_KEYS.lyric),
|
|
5586
5669
|
cueTimingUrl: await resolve(PAGE_META_KEYS.cue),
|
|
5587
5670
|
lyricPopupsEnabled: meta[PAGE_META_KEYS.popups] === true || String(meta[PAGE_META_KEYS.popups]) === "1",
|
|
@@ -6122,7 +6205,8 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
|
|
|
6122
6205
|
error: { get: () => read().error || null },
|
|
6123
6206
|
element: { value: isNative ? master : null },
|
|
6124
6207
|
activePlayer: { get: () => delegate ? delegate.activePlayer : active.element },
|
|
6125
|
-
masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id }
|
|
6208
|
+
masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id },
|
|
6209
|
+
masterSourceUrl: { get: () => delegate ? delegate.masterSourceUrl : reference?.sourceUrl || master.currentSrc || master.src || "" }
|
|
6126
6210
|
});
|
|
6127
6211
|
transport.matches = (selector) => target.matches(selector);
|
|
6128
6212
|
transport.getAttribute = (name) => target.getAttribute(name);
|
|
@@ -7038,7 +7122,7 @@ function installChildPlayback(scope, api, host) {
|
|
|
7038
7122
|
}
|
|
7039
7123
|
|
|
7040
7124
|
// .build/modules/version.mjs
|
|
7041
|
-
var VERSION = "3.
|
|
7125
|
+
var VERSION = "3.3.0";
|
|
7042
7126
|
|
|
7043
7127
|
// .build/modules/index.mjs
|
|
7044
7128
|
var mounted = /* @__PURE__ */ new WeakMap();
|
package/lib/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export * from './rudiments';
|
|
|
2
2
|
export type Remove = () => void;
|
|
3
3
|
export type PlaybackMedia = HTMLMediaElement | PlaybackSource;
|
|
4
4
|
export interface PlaybackMapping {offset?:number;duration?:number;playerStart?:number}
|
|
5
|
-
export type MasterReference = HTMLMediaElement | Readonly<{id:string;duration:number;element?:HTMLMediaElement}>;
|
|
5
|
+
export type MasterReference = HTMLMediaElement | Readonly<{id:string;duration:number;element?:HTMLMediaElement;sourceUrl?:string}>;
|
|
6
6
|
export interface PlaybackSource extends EventTarget {
|
|
7
7
|
readonly nodeType:1;readonly tagName:string;readonly element:HTMLMediaElement|null;readonly activePlayer:HTMLMediaElement|HTMLIFrameElement|null;
|
|
8
8
|
currentTime:number;readonly duration:number;readonly paused:boolean;readonly ended:boolean;readonly seeking:boolean;readonly playbackRate:number;
|
|
@@ -26,8 +26,15 @@ export function createPlaybackPublisher(options:ChildPlaybackPublisherOptions):C
|
|
|
26
26
|
export function createPlaybackReceiver(options:ChildPlaybackReceiverOptions):ChildPlaybackReceiver;
|
|
27
27
|
export type Offset = Readonly<{x:number;y:number;z?:number}>;
|
|
28
28
|
export type Metadata = string | {id?:string;handlerId?:string};
|
|
29
|
+
export interface BeatAlignment {
|
|
30
|
+
schema:'stemlab.beat-alignment.v1';source_sha256:string;source_url:string;source_audio_verified:true;
|
|
31
|
+
source_audio:Readonly<{sample_rate:number;num_frames:number;num_channels?:number;duration_seconds?:number}>;
|
|
32
|
+
bpm:number;exact_bpm:string;bpm_basis:string;bpm_status:'canonical'|'estimated';phase_status:'estimated'|'confirmed'|'synthetic';
|
|
33
|
+
analysis_id:string;evidence_sha256:string;report_sha256:string;first_beat_seconds:number;first_beat_exact_seconds:string;
|
|
34
|
+
offset_pips:number;exact_offset_pips:string;pips_per_beat:64;canonical_first_beat_pip:0;
|
|
35
|
+
}
|
|
29
36
|
export interface PageConfig {
|
|
30
|
-
pageId?:number;bpm?:number|null;bpmSource?:'explicit'|'fallback';lyricTimingUrl?:string;cueTimingUrl?:string;
|
|
37
|
+
pageId?:number;bpm?:number|null;bpmSource?:'explicit'|'fallback'|'analysis';beatAlignment?:BeatAlignment|null;lyricTimingUrl?:string;cueTimingUrl?:string;
|
|
31
38
|
lyricPopupsEnabled?:boolean;masterDurationMilliseconds?:number;sharedControlTicks?:number;lyricDisclosureTicks?:number;
|
|
32
39
|
effect?:''|'paper-planes';diagnostics?:boolean;
|
|
33
40
|
}
|
|
@@ -77,6 +84,8 @@ export interface DanceMoves {
|
|
|
77
84
|
readonly version:string;readonly bpm:number;readonly bpmSource:string;readonly ticksPerBeat:16;readonly root:HTMLElement;readonly config:Readonly<PageConfig>;
|
|
78
85
|
readonly destroyed:boolean;ready:Promise<DanceMoves>;
|
|
79
86
|
quantizeTicks(value:number):number;durationMilliseconds(ticks:number,bpmOverride?:number):number;currentTick(options?:Clock):number;
|
|
87
|
+
sourceTick(audio:PlaybackMedia,bpmOverride?:number):number;rhythmicTime(audio:PlaybackMedia):number;
|
|
88
|
+
beatAlignmentState(audio:PlaybackMedia):Readonly<{status:string;applied:boolean;firstBeatSeconds?:number;offsetPips?:number;sourceSha256?:string;bpmStatus?:string}>;
|
|
80
89
|
nextIntervalTick(interval:number,fromTick:number,strictlyFuture?:boolean):number;
|
|
81
90
|
scheduleAtInterval(interval:number,callback:(detail:IntervalDetail)=>void,options?:Clock):Remove;
|
|
82
91
|
deferStart(target:HTMLElement,interval:number,options?:Clock & {start?:(detail:IntervalDetail)=>void}):Remove;
|
|
@@ -104,7 +113,8 @@ export interface DanceMoves {
|
|
|
104
113
|
}
|
|
105
114
|
export const VERSION:string;
|
|
106
115
|
export const ORIENTATION_ADAPTERS:readonly OrientationAdapter[];
|
|
107
|
-
export const PAGE_META_KEYS:Readonly<{bpm:string;lyric:string;cue:string;popups:string;duration:string;effect:string}>;
|
|
116
|
+
export const PAGE_META_KEYS:Readonly<{bpm:string;lyric:string;cue:string;popups:string;duration:string;effect:string;alignment:string;alignmentAudio:string}>;
|
|
117
|
+
export function validateBeatAlignment(input?:BeatAlignment|null):Readonly<BeatAlignment>|null;
|
|
108
118
|
export function createDanceMoves(options:Options):DanceMoves;
|
|
109
119
|
export function validatePageConfig(input?:PageConfig):Readonly<PageConfig>;
|
|
110
120
|
export function fromWordPressMeta(meta:Record<string,unknown>,resolveAttachment?:(id:number)=>string|Promise<string>):Promise<Readonly<PageConfig>>;
|
package/lib/index.mjs
CHANGED
|
@@ -102,6 +102,38 @@ function install(__dmScope) {
|
|
|
102
102
|
return ticks;
|
|
103
103
|
}
|
|
104
104
|
var bpm = effectiveBpm(rawConfig.bpm);
|
|
105
|
+
var bpmSource = ["explicit", "analysis"].indexOf(rawConfig.bpmSource) >= 0 ? rawConfig.bpmSource : "fallback";
|
|
106
|
+
function beatAlignmentState(audio) {
|
|
107
|
+
var reading = rawConfig.beatAlignment;
|
|
108
|
+
if (!reading)
|
|
109
|
+
return { status: rawConfig.beatAlignmentStatus === "invalid" ? "invalid" : "unconfigured", applied: false };
|
|
110
|
+
var source = playbackAudio(audio);
|
|
111
|
+
var identity2 = source && (source.masterSourceUrl || source.masterId || source.currentSrc || source.src);
|
|
112
|
+
try {
|
|
113
|
+
if (!identity2 || new URL(String(identity2), document2.baseURI).href !== new URL(reading.source_url).href) {
|
|
114
|
+
return { status: "source-mismatch", applied: false };
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return { status: "source-mismatch", applied: false };
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
status: reading.phase_status,
|
|
121
|
+
applied: true,
|
|
122
|
+
firstBeatSeconds: reading.first_beat_seconds,
|
|
123
|
+
offsetPips: reading.offset_pips,
|
|
124
|
+
sourceSha256: reading.source_sha256,
|
|
125
|
+
bpmStatus: reading.bpm_status
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function rhythmicTime(audio) {
|
|
129
|
+
var source = playbackAudio(audio);
|
|
130
|
+
var seconds = source && Number.isFinite(Number(source.currentTime)) ? Number(source.currentTime) : 0;
|
|
131
|
+
var state = beatAlignmentState(source);
|
|
132
|
+
return seconds - (state.applied ? state.firstBeatSeconds : 0);
|
|
133
|
+
}
|
|
134
|
+
function sourceTick(audio, bpmOverride) {
|
|
135
|
+
return rhythmicTime(audio) * effectiveBpm(bpmOverride || bpm) * TICKS_PER_BEAT / 60;
|
|
136
|
+
}
|
|
105
137
|
function durationMilliseconds(ticks, bpmOverride) {
|
|
106
138
|
return 3750 / effectiveBpm(bpmOverride || bpm) * quantizeTicks(ticks);
|
|
107
139
|
}
|
|
@@ -109,7 +141,7 @@ function install(__dmScope) {
|
|
|
109
141
|
var settings = options || {};
|
|
110
142
|
var audio = playbackAudio(settings.audio) || (settings.clock === "audio" ? activeAudio : null);
|
|
111
143
|
if (audio && !audio.paused && !audio.ended && Number.isFinite(Number(audio.currentTime))) {
|
|
112
|
-
return
|
|
144
|
+
return sourceTick(audio, settings.bpm);
|
|
113
145
|
}
|
|
114
146
|
var now = window2.performance && typeof window2.performance.now === "function" ? window2.performance.now() : Date.now();
|
|
115
147
|
return Math.max(0, now - pageClockStarted) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 6e4;
|
|
@@ -408,7 +440,7 @@ function install(__dmScope) {
|
|
|
408
440
|
__dmScope.dataset(document2.documentElement, "danceMovesSharedControls", null);
|
|
409
441
|
}
|
|
410
442
|
__dmScope.dataset(document2.documentElement, "danceMovesBpm", String(bpm));
|
|
411
|
-
__dmScope.dataset(document2.documentElement, "danceMovesBpmSource",
|
|
443
|
+
__dmScope.dataset(document2.documentElement, "danceMovesBpmSource", bpmSource);
|
|
412
444
|
__dmScope.dataset(document2.documentElement, "danceMovesVersion", String(rawConfig.version || ""));
|
|
413
445
|
}
|
|
414
446
|
function registerAnimationScope(root, selectors) {
|
|
@@ -944,7 +976,7 @@ function install(__dmScope) {
|
|
|
944
976
|
var root = document2.querySelector(".ks-epk");
|
|
945
977
|
if (root) {
|
|
946
978
|
__dmScope.dataset(root, "danceMovesBpm", String(bpm));
|
|
947
|
-
__dmScope.dataset(root, "danceMovesBpmSource",
|
|
979
|
+
__dmScope.dataset(root, "danceMovesBpmSource", bpmSource);
|
|
948
980
|
if (rawConfig.lyricTimingUrl)
|
|
949
981
|
__dmScope.dataset(root, "danceMovesLyricTiming", String(rawConfig.lyricTimingUrl));
|
|
950
982
|
if (rawConfig.cueTimingUrl)
|
|
@@ -973,11 +1005,14 @@ function install(__dmScope) {
|
|
|
973
1005
|
var api = {
|
|
974
1006
|
version: String(rawConfig.version || ""),
|
|
975
1007
|
bpm,
|
|
976
|
-
bpmSource
|
|
1008
|
+
bpmSource,
|
|
977
1009
|
ticksPerBeat: TICKS_PER_BEAT,
|
|
978
1010
|
quantizeTicks,
|
|
979
1011
|
durationMilliseconds,
|
|
980
1012
|
currentTick,
|
|
1013
|
+
sourceTick,
|
|
1014
|
+
rhythmicTime,
|
|
1015
|
+
beatAlignmentState,
|
|
981
1016
|
nextIntervalTick,
|
|
982
1017
|
scheduleAtInterval,
|
|
983
1018
|
deferStart,
|
|
@@ -1184,7 +1219,8 @@ function install2(__dmScope) {
|
|
|
1184
1219
|
function sync() {
|
|
1185
1220
|
var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
|
|
1186
1221
|
var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
|
|
1187
|
-
var
|
|
1222
|
+
var rhythmicSeconds = motion.rhythmicTime ? motion.rhythmicTime(audio) : finite3(audio.currentTime, 0);
|
|
1223
|
+
var phase = -((rhythmicSeconds / rate % duration + duration) % duration);
|
|
1188
1224
|
__dmScope.setStyle(state.root.style, prefix + "-duration", duration.toFixed(6) + "s");
|
|
1189
1225
|
__dmScope.setStyle(state.root.style, prefix + "-delay", phase.toFixed(6) + "s");
|
|
1190
1226
|
__dmScope.dataset(state.root, "danceMovesPulse", state.reduced.matches || state.forced.matches ? "inactive" : audio.paused ? "paused" : "playing");
|
|
@@ -1476,7 +1512,8 @@ function install2(__dmScope) {
|
|
|
1476
1512
|
function calculate(reason, staticFallback) {
|
|
1477
1513
|
var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
|
|
1478
1514
|
var time = Math.max(0, finite3(audio.currentTime, 0));
|
|
1479
|
-
var
|
|
1515
|
+
var rhythmicSeconds = motion.rhythmicTime ? motion.rhythmicTime(audio) : time;
|
|
1516
|
+
var phase = ((rhythmicSeconds - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
|
|
1480
1517
|
var localFrame = staticFallback ? staticFrame : Math.min(frameCount - 1, Math.floor(phase * frameCount + 1e-9));
|
|
1481
1518
|
var mapped = mappedFrame(localFrame);
|
|
1482
1519
|
return {
|
|
@@ -4895,8 +4932,9 @@ function install9(__dmScope) {
|
|
|
4895
4932
|
anchor = 0;
|
|
4896
4933
|
lastClock = mode;
|
|
4897
4934
|
const tempo = bpm();
|
|
4898
|
-
const
|
|
4899
|
-
|
|
4935
|
+
const sourceBeats = useAudio ? Math.max(0, Number.isFinite(audio.currentTime) ? audio.currentTime : 0) * tempo / 60 : motion.currentTick({ clock: "page" }) / 16;
|
|
4936
|
+
const beats = useAudio && motion.sourceTick ? motion.sourceTick(audio) / 16 : sourceBeats;
|
|
4937
|
+
return { beats, sourceBeats, mode, tempo };
|
|
4900
4938
|
}
|
|
4901
4939
|
function blocked() {
|
|
4902
4940
|
if (!enabled)
|
|
@@ -4934,7 +4972,8 @@ function install9(__dmScope) {
|
|
|
4934
4972
|
audio: s.mode === "audio" ? audio : null,
|
|
4935
4973
|
rudiment: row.name,
|
|
4936
4974
|
clock: s.mode,
|
|
4937
|
-
sourceBeats: s.
|
|
4975
|
+
sourceBeats: s.sourceBeats,
|
|
4976
|
+
rhythmicBeats: s.beats,
|
|
4938
4977
|
bpm: s.tempo,
|
|
4939
4978
|
rate,
|
|
4940
4979
|
pip: neutral ? null : pip,
|
|
@@ -5480,6 +5519,35 @@ function createScope(root, config, { legacyGlobals = false, nonce, orientation =
|
|
|
5480
5519
|
return api;
|
|
5481
5520
|
}
|
|
5482
5521
|
|
|
5522
|
+
// .build/modules/beat-alignment.mjs
|
|
5523
|
+
var hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
5524
|
+
function rational(value, label) {
|
|
5525
|
+
if (typeof value !== "string" || !/^-?\d{1,80}(?:\/[1-9]\d{0,79})?$/.test(value)) throw new TypeError(`Invalid exact ${label}`);
|
|
5526
|
+
const [n, d = "1"] = value.split("/");
|
|
5527
|
+
return [BigInt(n), BigInt(d)];
|
|
5528
|
+
}
|
|
5529
|
+
function numeric(value, label, min, max) {
|
|
5530
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`Invalid ${label}`);
|
|
5531
|
+
return value;
|
|
5532
|
+
}
|
|
5533
|
+
function validateBeatAlignment(input) {
|
|
5534
|
+
if (input === void 0 || input === null || input === "") return null;
|
|
5535
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || input.schema !== "stemlab.beat-alignment.v1" || input.pips_per_beat !== 64 || input.canonical_first_beat_pip !== 0 || !hash(input.source_sha256) || !hash(input.evidence_sha256) || !hash(input.report_sha256)) throw new TypeError("Invalid source-bound StemLab beat alignment");
|
|
5536
|
+
const bpm = numeric(input.bpm, "alignment BPM", 20, 400);
|
|
5537
|
+
const first = numeric(input.first_beat_seconds, "first beat", 0, 86400);
|
|
5538
|
+
const offset = numeric(input.offset_pips, "pip offset", -36864e3, 0);
|
|
5539
|
+
const [bn, bd] = rational(input.exact_bpm, "BPM"), [fn, fd] = rational(input.first_beat_exact_seconds, "first beat"), [pn, pd] = rational(input.exact_offset_pips, "pip offset");
|
|
5540
|
+
if (fn * bn * 64n * pd + pn * fd * bd * 60n !== 0n || bn <= 0n || fn < 0n || Math.abs(Number(bn) / Number(bd) - bpm) > 1e-9 || Math.abs(Number(fn) / Number(fd) - first) > 1e-9 || Math.abs(Number(pn) / Number(pd) - offset) > 1e-7) throw new RangeError("Exact beat alignment and numeric clock disagree");
|
|
5541
|
+
if (!["canonical", "estimated"].includes(input.bpm_status) || !["estimated", "confirmed", "synthetic"].includes(input.phase_status)) throw new TypeError("Alignment authority is required");
|
|
5542
|
+
for (const key of ["analysis_id", "bpm_basis"]) if (typeof input[key] !== "string" || !/^[a-zA-Z0-9_.-]{1,128}$/.test(input[key])) throw new TypeError(`Invalid ${key}`);
|
|
5543
|
+
if (input.source_audio_verified !== true || !input.source_audio || !Number.isSafeInteger(input.source_audio.num_frames) || input.source_audio.num_frames < 1 || !Number.isSafeInteger(input.source_audio.sample_rate) || input.source_audio.sample_rate < 1 || first >= input.source_audio.num_frames / input.source_audio.sample_rate) throw new TypeError("Verified source audio frame bounds are required");
|
|
5544
|
+
const sourceUrl = input.source_url;
|
|
5545
|
+
if (typeof sourceUrl !== "string" || sourceUrl.length > 4096 || /[\u0000-\u001f]/.test(sourceUrl)) throw new TypeError("An exact verified master URL is required");
|
|
5546
|
+
const parsed = new URL(sourceUrl);
|
|
5547
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) throw new TypeError("Invalid verified master URL");
|
|
5548
|
+
return Object.freeze({ ...input, bpm, first_beat_seconds: first, offset_pips: offset, source_audio: Object.freeze({ ...input.source_audio }) });
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5483
5551
|
// .build/modules/config.mjs
|
|
5484
5552
|
var PAGE_META_KEYS = Object.freeze({
|
|
5485
5553
|
bpm: "_dance_moves_bpm",
|
|
@@ -5487,7 +5555,9 @@ var PAGE_META_KEYS = Object.freeze({
|
|
|
5487
5555
|
cue: "_dance_moves_cue_timing_id",
|
|
5488
5556
|
popups: "_dance_moves_lyric_popups_enabled",
|
|
5489
5557
|
duration: "_dance_moves_master_duration_ms",
|
|
5490
|
-
effect: "_dance_moves_effect"
|
|
5558
|
+
effect: "_dance_moves_effect",
|
|
5559
|
+
alignment: "_dance_moves_beat_alignment",
|
|
5560
|
+
alignmentAudio: "_dance_moves_beat_alignment_audio_id"
|
|
5491
5561
|
});
|
|
5492
5562
|
var object = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
5493
5563
|
function finite(value, name, min, max) {
|
|
@@ -5506,7 +5576,11 @@ function url(value, name) {
|
|
|
5506
5576
|
function validatePageConfig(input = {}) {
|
|
5507
5577
|
if (!object(input)) throw new TypeError("Page configuration must be an object");
|
|
5508
5578
|
const explicit = input.bpm !== void 0 && input.bpm !== null && input.bpm !== "";
|
|
5509
|
-
const
|
|
5579
|
+
const beatAlignment = validateBeatAlignment(input.beatAlignment);
|
|
5580
|
+
const canonicalExplicit = explicit && !["fallback", "analysis"].includes(input.bpmSource);
|
|
5581
|
+
if (canonicalExplicit && beatAlignment && Math.abs(input.bpm - beatAlignment.bpm) > 500001e-9) throw new RangeError("Stored BPM and selected beat alignment disagree; select a matching BPM basis");
|
|
5582
|
+
const bpm = beatAlignment ? beatAlignment.bpm : explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
|
|
5583
|
+
if (explicit) finite(input.bpm, "bpm", 20, 400);
|
|
5510
5584
|
const effect = input.effect ?? "";
|
|
5511
5585
|
if (!["", "paper-planes"].includes(effect)) throw new RangeError("effect must be empty or paper-planes");
|
|
5512
5586
|
if (input.lyricPopupsEnabled !== void 0 && typeof input.lyricPopupsEnabled !== "boolean") throw new TypeError("lyricPopupsEnabled must be boolean");
|
|
@@ -5518,8 +5592,9 @@ function validatePageConfig(input = {}) {
|
|
|
5518
5592
|
return Object.freeze({
|
|
5519
5593
|
pageId,
|
|
5520
5594
|
bpm,
|
|
5521
|
-
bpmSource:
|
|
5595
|
+
bpmSource: canonicalExplicit ? "explicit" : beatAlignment ? "analysis" : "fallback",
|
|
5522
5596
|
fallbackBpm: 120,
|
|
5597
|
+
beatAlignment,
|
|
5523
5598
|
ticksPerBeat: 16,
|
|
5524
5599
|
tickDefinition: "sixteenth-of-beat",
|
|
5525
5600
|
longDurationQuantumTicks: 16,
|
|
@@ -5535,9 +5610,9 @@ function validatePageConfig(input = {}) {
|
|
|
5535
5610
|
}
|
|
5536
5611
|
async function fromWordPressMeta(meta, resolveAttachment) {
|
|
5537
5612
|
if (!object(meta)) throw new TypeError("meta must be an object");
|
|
5538
|
-
const
|
|
5613
|
+
const numeric2 = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
|
|
5539
5614
|
const b = meta[PAGE_META_KEYS.bpm];
|
|
5540
|
-
const bpm =
|
|
5615
|
+
const bpm = numeric2(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
|
|
5541
5616
|
const resolve = async (key) => {
|
|
5542
5617
|
const value = Number(meta[key] || 0);
|
|
5543
5618
|
if (!Number.isSafeInteger(value)) throw new RangeError("Attachment ID must be a safe integer");
|
|
@@ -5547,8 +5622,15 @@ async function fromWordPressMeta(meta, resolveAttachment) {
|
|
|
5547
5622
|
return await resolveAttachment(id) || "";
|
|
5548
5623
|
};
|
|
5549
5624
|
const duration = Number(meta[PAGE_META_KEYS.duration]);
|
|
5625
|
+
let beatAlignment = meta[PAGE_META_KEYS.alignment] || null;
|
|
5626
|
+
if (typeof beatAlignment === "string") {
|
|
5627
|
+
if (beatAlignment.length > 16384) throw new RangeError("Beat alignment exceeds 16 KiB");
|
|
5628
|
+
beatAlignment = JSON.parse(beatAlignment);
|
|
5629
|
+
}
|
|
5630
|
+
if (beatAlignment) beatAlignment = { ...beatAlignment, source_url: await resolve(PAGE_META_KEYS.alignmentAudio) };
|
|
5550
5631
|
return validatePageConfig({
|
|
5551
5632
|
bpm,
|
|
5633
|
+
beatAlignment,
|
|
5552
5634
|
lyricTimingUrl: await resolve(PAGE_META_KEYS.lyric),
|
|
5553
5635
|
cueTimingUrl: await resolve(PAGE_META_KEYS.cue),
|
|
5554
5636
|
lyricPopupsEnabled: meta[PAGE_META_KEYS.popups] === true || String(meta[PAGE_META_KEYS.popups]) === "1",
|
|
@@ -6089,7 +6171,8 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
|
|
|
6089
6171
|
error: { get: () => read().error || null },
|
|
6090
6172
|
element: { value: isNative ? master : null },
|
|
6091
6173
|
activePlayer: { get: () => delegate ? delegate.activePlayer : active.element },
|
|
6092
|
-
masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id }
|
|
6174
|
+
masterId: { get: () => delegate ? delegate.masterId : master.currentSrc || master.src || master.id },
|
|
6175
|
+
masterSourceUrl: { get: () => delegate ? delegate.masterSourceUrl : reference?.sourceUrl || master.currentSrc || master.src || "" }
|
|
6093
6176
|
});
|
|
6094
6177
|
transport.matches = (selector) => target.matches(selector);
|
|
6095
6178
|
transport.getAttribute = (name) => target.getAttribute(name);
|
|
@@ -7005,7 +7088,7 @@ function installChildPlayback(scope, api, host) {
|
|
|
7005
7088
|
}
|
|
7006
7089
|
|
|
7007
7090
|
// .build/modules/version.mjs
|
|
7008
|
-
var VERSION = "3.
|
|
7091
|
+
var VERSION = "3.3.0";
|
|
7009
7092
|
|
|
7010
7093
|
// .build/modules/index.mjs
|
|
7011
7094
|
var mounted = /* @__PURE__ */ new WeakMap();
|
|
@@ -7100,5 +7183,6 @@ export {
|
|
|
7100
7183
|
createPlaybackPublisher,
|
|
7101
7184
|
createPlaybackReceiver,
|
|
7102
7185
|
fromWordPressMeta,
|
|
7186
|
+
validateBeatAlignment,
|
|
7103
7187
|
validatePageConfig
|
|
7104
7188
|
};
|
package/lib/react.cjs
CHANGED
|
@@ -115,7 +115,11 @@ function Rudiment({ runtime, options, children, ...attributes }) {
|
|
|
115
115
|
}
|
|
116
116
|
function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
117
117
|
const [draft, setDraft] = (0, import_react.useState)(() => ({ ...value })), [message, setMessage] = (0, import_react.useState)(""), [busy, setBusy] = (0, import_react.useState)(false);
|
|
118
|
-
(0, import_react.
|
|
118
|
+
const [alignmentText, setAlignmentText] = (0, import_react.useState)(() => value.beatAlignment ? JSON.stringify(value.beatAlignment, null, 2) : "");
|
|
119
|
+
(0, import_react.useEffect)(() => {
|
|
120
|
+
setDraft({ ...value });
|
|
121
|
+
setAlignmentText(value.beatAlignment ? JSON.stringify(value.beatAlignment, null, 2) : "");
|
|
122
|
+
}, [value]);
|
|
119
123
|
const input = (key, label, type = "text") => (0, import_react.createElement)("label", { key }, label, (0, import_react.createElement)("input", {
|
|
120
124
|
type,
|
|
121
125
|
value: draft[key] ?? "",
|
|
@@ -129,7 +133,7 @@ function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
|
129
133
|
setBusy(true);
|
|
130
134
|
setMessage("");
|
|
131
135
|
try {
|
|
132
|
-
await onSave((0, import_index.validatePageConfig)(draft));
|
|
136
|
+
await onSave((0, import_index.validatePageConfig)({ ...draft, beatAlignment: alignmentText.trim() ? JSON.parse(alignmentText) : null }));
|
|
133
137
|
setMessage("Saved");
|
|
134
138
|
} catch (error) {
|
|
135
139
|
setMessage(error.message);
|
|
@@ -140,6 +144,7 @@ function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
|
140
144
|
input("bpm", "BPM", "number"),
|
|
141
145
|
input("lyricTimingUrl", "Lyric timing URL"),
|
|
142
146
|
input("cueTimingUrl", "Cue timing URL"),
|
|
147
|
+
(0, import_react.createElement)("label", null, "Beat alignment reading (JSON)", (0, import_react.createElement)("textarea", { rows: 7, value: alignmentText, onChange: (e) => setAlignmentText(e.target.value) })),
|
|
143
148
|
input("masterDurationMilliseconds", "Master duration (milliseconds)", "number"),
|
|
144
149
|
(0, import_react.createElement)("label", null, (0, import_react.createElement)("input", { type: "checkbox", checked: !!draft.lyricPopupsEnabled, onChange: (e) => setDraft({ ...draft, lyricPopupsEnabled: e.target.checked }) }), "Timed lyric pop-ups"),
|
|
145
150
|
(0, import_react.createElement)("label", null, "Ambient effect", (0, import_react.createElement)(
|
package/lib/react.mjs
CHANGED
|
@@ -86,7 +86,11 @@ function Rudiment({ runtime, options, children, ...attributes }) {
|
|
|
86
86
|
}
|
|
87
87
|
function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
88
88
|
const [draft, setDraft] = useState(() => ({ ...value })), [message, setMessage] = useState(""), [busy, setBusy] = useState(false);
|
|
89
|
-
|
|
89
|
+
const [alignmentText, setAlignmentText] = useState(() => value.beatAlignment ? JSON.stringify(value.beatAlignment, null, 2) : "");
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setDraft({ ...value });
|
|
92
|
+
setAlignmentText(value.beatAlignment ? JSON.stringify(value.beatAlignment, null, 2) : "");
|
|
93
|
+
}, [value]);
|
|
90
94
|
const input = (key, label, type = "text") => h("label", { key }, label, h("input", {
|
|
91
95
|
type,
|
|
92
96
|
value: draft[key] ?? "",
|
|
@@ -100,7 +104,7 @@ function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
|
100
104
|
setBusy(true);
|
|
101
105
|
setMessage("");
|
|
102
106
|
try {
|
|
103
|
-
await onSave(validatePageConfig(draft));
|
|
107
|
+
await onSave(validatePageConfig({ ...draft, beatAlignment: alignmentText.trim() ? JSON.parse(alignmentText) : null }));
|
|
104
108
|
setMessage("Saved");
|
|
105
109
|
} catch (error) {
|
|
106
110
|
setMessage(error.message);
|
|
@@ -111,6 +115,7 @@ function PageMetadataEditor({ value = EMPTY, onSave }) {
|
|
|
111
115
|
input("bpm", "BPM", "number"),
|
|
112
116
|
input("lyricTimingUrl", "Lyric timing URL"),
|
|
113
117
|
input("cueTimingUrl", "Cue timing URL"),
|
|
118
|
+
h("label", null, "Beat alignment reading (JSON)", h("textarea", { rows: 7, value: alignmentText, onChange: (e) => setAlignmentText(e.target.value) })),
|
|
114
119
|
input("masterDurationMilliseconds", "Master duration (milliseconds)", "number"),
|
|
115
120
|
h("label", null, h("input", { type: "checkbox", checked: !!draft.lyricPopupsEnabled, onChange: (e) => setDraft({ ...draft, lyricPopupsEnabled: e.target.checked }) }), "Timed lyric pop-ups"),
|
|
116
121
|
h("label", null, "Ambient effect", h(
|