@kieransimkin/dancemoves 3.1.23 → 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 +18 -0
- package/docs/audio-recording-playback.md +56 -0
- 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 +340 -77
- package/lib/index.d.ts +18 -4
- package/lib/index.mjs +340 -77
- 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/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(
|
package/lib/server.cjs
CHANGED
|
@@ -48,6 +48,35 @@ var import_node_fs = require("node:fs");
|
|
|
48
48
|
var import_node_path = __toESM(require("node:path"), 1);
|
|
49
49
|
var import_node_stream = require("node:stream");
|
|
50
50
|
|
|
51
|
+
// .build/modules/beat-alignment.mjs
|
|
52
|
+
var hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
53
|
+
function rational(value, label) {
|
|
54
|
+
if (typeof value !== "string" || !/^-?\d{1,80}(?:\/[1-9]\d{0,79})?$/.test(value)) throw new TypeError(`Invalid exact ${label}`);
|
|
55
|
+
const [n, d = "1"] = value.split("/");
|
|
56
|
+
return [BigInt(n), BigInt(d)];
|
|
57
|
+
}
|
|
58
|
+
function numeric(value, label, min, max) {
|
|
59
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`Invalid ${label}`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
function validateBeatAlignment(input) {
|
|
63
|
+
if (input === void 0 || input === null || input === "") return null;
|
|
64
|
+
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");
|
|
65
|
+
const bpm = numeric(input.bpm, "alignment BPM", 20, 400);
|
|
66
|
+
const first = numeric(input.first_beat_seconds, "first beat", 0, 86400);
|
|
67
|
+
const offset = numeric(input.offset_pips, "pip offset", -36864e3, 0);
|
|
68
|
+
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");
|
|
69
|
+
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");
|
|
70
|
+
if (!["canonical", "estimated"].includes(input.bpm_status) || !["estimated", "confirmed", "synthetic"].includes(input.phase_status)) throw new TypeError("Alignment authority is required");
|
|
71
|
+
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}`);
|
|
72
|
+
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");
|
|
73
|
+
const sourceUrl = input.source_url;
|
|
74
|
+
if (typeof sourceUrl !== "string" || sourceUrl.length > 4096 || /[\u0000-\u001f]/.test(sourceUrl)) throw new TypeError("An exact verified master URL is required");
|
|
75
|
+
const parsed = new URL(sourceUrl);
|
|
76
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) throw new TypeError("Invalid verified master URL");
|
|
77
|
+
return Object.freeze({ ...input, bpm, first_beat_seconds: first, offset_pips: offset, source_audio: Object.freeze({ ...input.source_audio }) });
|
|
78
|
+
}
|
|
79
|
+
|
|
51
80
|
// .build/modules/config.mjs
|
|
52
81
|
var PAGE_META_KEYS = Object.freeze({
|
|
53
82
|
bpm: "_dance_moves_bpm",
|
|
@@ -55,7 +84,9 @@ var PAGE_META_KEYS = Object.freeze({
|
|
|
55
84
|
cue: "_dance_moves_cue_timing_id",
|
|
56
85
|
popups: "_dance_moves_lyric_popups_enabled",
|
|
57
86
|
duration: "_dance_moves_master_duration_ms",
|
|
58
|
-
effect: "_dance_moves_effect"
|
|
87
|
+
effect: "_dance_moves_effect",
|
|
88
|
+
alignment: "_dance_moves_beat_alignment",
|
|
89
|
+
alignmentAudio: "_dance_moves_beat_alignment_audio_id"
|
|
59
90
|
});
|
|
60
91
|
var object = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
61
92
|
function finite(value, name, min, max) {
|
|
@@ -74,7 +105,11 @@ function url(value, name) {
|
|
|
74
105
|
function validatePageConfig(input = {}) {
|
|
75
106
|
if (!object(input)) throw new TypeError("Page configuration must be an object");
|
|
76
107
|
const explicit = input.bpm !== void 0 && input.bpm !== null && input.bpm !== "";
|
|
77
|
-
const
|
|
108
|
+
const beatAlignment = validateBeatAlignment(input.beatAlignment);
|
|
109
|
+
const canonicalExplicit = explicit && !["fallback", "analysis"].includes(input.bpmSource);
|
|
110
|
+
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");
|
|
111
|
+
const bpm = beatAlignment ? beatAlignment.bpm : explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
|
|
112
|
+
if (explicit) finite(input.bpm, "bpm", 20, 400);
|
|
78
113
|
const effect = input.effect ?? "";
|
|
79
114
|
if (!["", "paper-planes"].includes(effect)) throw new RangeError("effect must be empty or paper-planes");
|
|
80
115
|
if (input.lyricPopupsEnabled !== void 0 && typeof input.lyricPopupsEnabled !== "boolean") throw new TypeError("lyricPopupsEnabled must be boolean");
|
|
@@ -86,8 +121,9 @@ function validatePageConfig(input = {}) {
|
|
|
86
121
|
return Object.freeze({
|
|
87
122
|
pageId,
|
|
88
123
|
bpm,
|
|
89
|
-
bpmSource:
|
|
124
|
+
bpmSource: canonicalExplicit ? "explicit" : beatAlignment ? "analysis" : "fallback",
|
|
90
125
|
fallbackBpm: 120,
|
|
126
|
+
beatAlignment,
|
|
91
127
|
ticksPerBeat: 16,
|
|
92
128
|
tickDefinition: "sixteenth-of-beat",
|
|
93
129
|
longDurationQuantumTicks: 16,
|
|
@@ -103,9 +139,9 @@ function validatePageConfig(input = {}) {
|
|
|
103
139
|
}
|
|
104
140
|
async function fromWordPressMeta(meta, resolveAttachment) {
|
|
105
141
|
if (!object(meta)) throw new TypeError("meta must be an object");
|
|
106
|
-
const
|
|
142
|
+
const numeric3 = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
|
|
107
143
|
const b = meta[PAGE_META_KEYS.bpm];
|
|
108
|
-
const bpm =
|
|
144
|
+
const bpm = numeric3(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
|
|
109
145
|
const resolve = async (key) => {
|
|
110
146
|
const value = Number(meta[key] || 0);
|
|
111
147
|
if (!Number.isSafeInteger(value)) throw new RangeError("Attachment ID must be a safe integer");
|
|
@@ -115,8 +151,15 @@ async function fromWordPressMeta(meta, resolveAttachment) {
|
|
|
115
151
|
return await resolveAttachment(id) || "";
|
|
116
152
|
};
|
|
117
153
|
const duration = Number(meta[PAGE_META_KEYS.duration]);
|
|
154
|
+
let beatAlignment = meta[PAGE_META_KEYS.alignment] || null;
|
|
155
|
+
if (typeof beatAlignment === "string") {
|
|
156
|
+
if (beatAlignment.length > 16384) throw new RangeError("Beat alignment exceeds 16 KiB");
|
|
157
|
+
beatAlignment = JSON.parse(beatAlignment);
|
|
158
|
+
}
|
|
159
|
+
if (beatAlignment) beatAlignment = { ...beatAlignment, source_url: await resolve(PAGE_META_KEYS.alignmentAudio) };
|
|
118
160
|
return validatePageConfig({
|
|
119
161
|
bpm,
|
|
162
|
+
beatAlignment,
|
|
120
163
|
lyricTimingUrl: await resolve(PAGE_META_KEYS.lyric),
|
|
121
164
|
cueTimingUrl: await resolve(PAGE_META_KEYS.cue),
|
|
122
165
|
lyricPopupsEnabled: meta[PAGE_META_KEYS.popups] === true || String(meta[PAGE_META_KEYS.popups]) === "1",
|
|
@@ -286,7 +329,7 @@ function createDownloadService({ rootDirectory, secret, origin, route = "/api/do
|
|
|
286
329
|
}
|
|
287
330
|
return Object.freeze({ sign, handle });
|
|
288
331
|
}
|
|
289
|
-
var
|
|
332
|
+
var numeric2 = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
|
|
290
333
|
function boundedText(value, max = 500) {
|
|
291
334
|
return String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, max);
|
|
292
335
|
}
|
|
@@ -294,13 +337,13 @@ function validateMotionCapture(payload) {
|
|
|
294
337
|
if (!payload || payload.schema !== "ks-epk-motion-recording/v1") fail("ks_motion_schema", "Unsupported capture schema");
|
|
295
338
|
if (!Array.isArray(payload.samples) || payload.samples.length < 1 || payload.samples.length > 500) fail("ks_motion_samples", "A capture must contain 1..500 samples");
|
|
296
339
|
const samples = payload.samples.map((sample) => {
|
|
297
|
-
if (!sample || !
|
|
340
|
+
if (!sample || !numeric2(sample.milliseconds, 0, 12e4) || !numeric2(sample.beta, -180, 180) || !numeric2(sample.gamma, -90, 90)) fail("ks_motion_invalid_samples", "Invalid motion sample");
|
|
298
341
|
return {
|
|
299
342
|
milliseconds: sample.milliseconds,
|
|
300
343
|
beta: sample.beta,
|
|
301
344
|
gamma: sample.gamma,
|
|
302
|
-
alpha: sample.alpha === null || sample.alpha === void 0 ? null :
|
|
303
|
-
screenAngle:
|
|
345
|
+
alpha: sample.alpha === null || sample.alpha === void 0 ? null : numeric2(sample.alpha, 0, 360) ? sample.alpha : null,
|
|
346
|
+
screenAngle: numeric2(sample.screenAngle ?? 0, -360, 360) ? sample.screenAngle || 0 : null,
|
|
304
347
|
...Object.fromEntries(["absolute", "nativeTrusted", "simulatedTrusted", "fieldParity", "dispatchParity", "mapperParity"].map((k) => [k, !!sample[k]]))
|
|
305
348
|
};
|
|
306
349
|
});
|
|
@@ -309,7 +352,7 @@ function validateMotionCapture(payload) {
|
|
|
309
352
|
capturedAt: boundedText(payload.capturedAt, 100),
|
|
310
353
|
userAgent: boundedText(payload.userAgent),
|
|
311
354
|
samples,
|
|
312
|
-
screenAngle:
|
|
355
|
+
screenAngle: numeric2(payload.screenAngle ?? 0, -360, 360) ? payload.screenAngle || 0 : null,
|
|
313
356
|
storedSampleCount: samples.length,
|
|
314
357
|
sampleStrategy: boundedText(payload.sampleStrategy || "legacy", 80).toLowerCase().replace(/[^a-z0-9_-]/g, ""),
|
|
315
358
|
fullWindowObserved: !!payload.fullWindowObserved,
|
|
@@ -321,7 +364,7 @@ function validateMotionCapture(payload) {
|
|
|
321
364
|
validPairs: Number.isFinite(Number(v.validPairs)) ? Math.abs(Math.trunc(Number(v.validPairs))) : null
|
|
322
365
|
}))
|
|
323
366
|
};
|
|
324
|
-
for (const key of ["targetDurationMilliseconds", "captureDurationMilliseconds", "sampledDurationMilliseconds", "storageIntervalMilliseconds"]) clean[key] =
|
|
367
|
+
for (const key of ["targetDurationMilliseconds", "captureDurationMilliseconds", "sampledDurationMilliseconds", "storageIntervalMilliseconds"]) clean[key] = numeric2(payload[key] ?? 0, 0, 12e4) ? payload[key] || 0 : null;
|
|
325
368
|
for (const key of ["processedPairCount", "failureCount"]) clean[key] = Math.min(1e5, Math.abs(Math.trunc(Number(payload[key]) || (key === "processedPairCount" ? samples.length : clean.failures.length))));
|
|
326
369
|
if (Buffer.byteLength(JSON.stringify(clean)) > 262144) fail("ks_motion_size", "Capture too large", 413);
|
|
327
370
|
return clean;
|
package/lib/server.mjs
CHANGED
|
@@ -5,6 +5,35 @@ import { constants as fsConstants } from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { Readable } from "node:stream";
|
|
7
7
|
|
|
8
|
+
// .build/modules/beat-alignment.mjs
|
|
9
|
+
var hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
10
|
+
function rational(value, label) {
|
|
11
|
+
if (typeof value !== "string" || !/^-?\d{1,80}(?:\/[1-9]\d{0,79})?$/.test(value)) throw new TypeError(`Invalid exact ${label}`);
|
|
12
|
+
const [n, d = "1"] = value.split("/");
|
|
13
|
+
return [BigInt(n), BigInt(d)];
|
|
14
|
+
}
|
|
15
|
+
function numeric(value, label, min, max) {
|
|
16
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) throw new RangeError(`Invalid ${label}`);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function validateBeatAlignment(input) {
|
|
20
|
+
if (input === void 0 || input === null || input === "") return null;
|
|
21
|
+
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");
|
|
22
|
+
const bpm = numeric(input.bpm, "alignment BPM", 20, 400);
|
|
23
|
+
const first = numeric(input.first_beat_seconds, "first beat", 0, 86400);
|
|
24
|
+
const offset = numeric(input.offset_pips, "pip offset", -36864e3, 0);
|
|
25
|
+
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");
|
|
26
|
+
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");
|
|
27
|
+
if (!["canonical", "estimated"].includes(input.bpm_status) || !["estimated", "confirmed", "synthetic"].includes(input.phase_status)) throw new TypeError("Alignment authority is required");
|
|
28
|
+
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}`);
|
|
29
|
+
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");
|
|
30
|
+
const sourceUrl = input.source_url;
|
|
31
|
+
if (typeof sourceUrl !== "string" || sourceUrl.length > 4096 || /[\u0000-\u001f]/.test(sourceUrl)) throw new TypeError("An exact verified master URL is required");
|
|
32
|
+
const parsed = new URL(sourceUrl);
|
|
33
|
+
if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.hash) throw new TypeError("Invalid verified master URL");
|
|
34
|
+
return Object.freeze({ ...input, bpm, first_beat_seconds: first, offset_pips: offset, source_audio: Object.freeze({ ...input.source_audio }) });
|
|
35
|
+
}
|
|
36
|
+
|
|
8
37
|
// .build/modules/config.mjs
|
|
9
38
|
var PAGE_META_KEYS = Object.freeze({
|
|
10
39
|
bpm: "_dance_moves_bpm",
|
|
@@ -12,7 +41,9 @@ var PAGE_META_KEYS = Object.freeze({
|
|
|
12
41
|
cue: "_dance_moves_cue_timing_id",
|
|
13
42
|
popups: "_dance_moves_lyric_popups_enabled",
|
|
14
43
|
duration: "_dance_moves_master_duration_ms",
|
|
15
|
-
effect: "_dance_moves_effect"
|
|
44
|
+
effect: "_dance_moves_effect",
|
|
45
|
+
alignment: "_dance_moves_beat_alignment",
|
|
46
|
+
alignmentAudio: "_dance_moves_beat_alignment_audio_id"
|
|
16
47
|
});
|
|
17
48
|
var object = (value) => value && typeof value === "object" && !Array.isArray(value);
|
|
18
49
|
function finite(value, name, min, max) {
|
|
@@ -31,7 +62,11 @@ function url(value, name) {
|
|
|
31
62
|
function validatePageConfig(input = {}) {
|
|
32
63
|
if (!object(input)) throw new TypeError("Page configuration must be an object");
|
|
33
64
|
const explicit = input.bpm !== void 0 && input.bpm !== null && input.bpm !== "";
|
|
34
|
-
const
|
|
65
|
+
const beatAlignment = validateBeatAlignment(input.beatAlignment);
|
|
66
|
+
const canonicalExplicit = explicit && !["fallback", "analysis"].includes(input.bpmSource);
|
|
67
|
+
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");
|
|
68
|
+
const bpm = beatAlignment ? beatAlignment.bpm : explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
|
|
69
|
+
if (explicit) finite(input.bpm, "bpm", 20, 400);
|
|
35
70
|
const effect = input.effect ?? "";
|
|
36
71
|
if (!["", "paper-planes"].includes(effect)) throw new RangeError("effect must be empty or paper-planes");
|
|
37
72
|
if (input.lyricPopupsEnabled !== void 0 && typeof input.lyricPopupsEnabled !== "boolean") throw new TypeError("lyricPopupsEnabled must be boolean");
|
|
@@ -43,8 +78,9 @@ function validatePageConfig(input = {}) {
|
|
|
43
78
|
return Object.freeze({
|
|
44
79
|
pageId,
|
|
45
80
|
bpm,
|
|
46
|
-
bpmSource:
|
|
81
|
+
bpmSource: canonicalExplicit ? "explicit" : beatAlignment ? "analysis" : "fallback",
|
|
47
82
|
fallbackBpm: 120,
|
|
83
|
+
beatAlignment,
|
|
48
84
|
ticksPerBeat: 16,
|
|
49
85
|
tickDefinition: "sixteenth-of-beat",
|
|
50
86
|
longDurationQuantumTicks: 16,
|
|
@@ -60,9 +96,9 @@ function validatePageConfig(input = {}) {
|
|
|
60
96
|
}
|
|
61
97
|
async function fromWordPressMeta(meta, resolveAttachment) {
|
|
62
98
|
if (!object(meta)) throw new TypeError("meta must be an object");
|
|
63
|
-
const
|
|
99
|
+
const numeric3 = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
|
|
64
100
|
const b = meta[PAGE_META_KEYS.bpm];
|
|
65
|
-
const bpm =
|
|
101
|
+
const bpm = numeric3(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
|
|
66
102
|
const resolve = async (key) => {
|
|
67
103
|
const value = Number(meta[key] || 0);
|
|
68
104
|
if (!Number.isSafeInteger(value)) throw new RangeError("Attachment ID must be a safe integer");
|
|
@@ -72,8 +108,15 @@ async function fromWordPressMeta(meta, resolveAttachment) {
|
|
|
72
108
|
return await resolveAttachment(id) || "";
|
|
73
109
|
};
|
|
74
110
|
const duration = Number(meta[PAGE_META_KEYS.duration]);
|
|
111
|
+
let beatAlignment = meta[PAGE_META_KEYS.alignment] || null;
|
|
112
|
+
if (typeof beatAlignment === "string") {
|
|
113
|
+
if (beatAlignment.length > 16384) throw new RangeError("Beat alignment exceeds 16 KiB");
|
|
114
|
+
beatAlignment = JSON.parse(beatAlignment);
|
|
115
|
+
}
|
|
116
|
+
if (beatAlignment) beatAlignment = { ...beatAlignment, source_url: await resolve(PAGE_META_KEYS.alignmentAudio) };
|
|
75
117
|
return validatePageConfig({
|
|
76
118
|
bpm,
|
|
119
|
+
beatAlignment,
|
|
77
120
|
lyricTimingUrl: await resolve(PAGE_META_KEYS.lyric),
|
|
78
121
|
cueTimingUrl: await resolve(PAGE_META_KEYS.cue),
|
|
79
122
|
lyricPopupsEnabled: meta[PAGE_META_KEYS.popups] === true || String(meta[PAGE_META_KEYS.popups]) === "1",
|
|
@@ -243,7 +286,7 @@ function createDownloadService({ rootDirectory, secret, origin, route = "/api/do
|
|
|
243
286
|
}
|
|
244
287
|
return Object.freeze({ sign, handle });
|
|
245
288
|
}
|
|
246
|
-
var
|
|
289
|
+
var numeric2 = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
|
|
247
290
|
function boundedText(value, max = 500) {
|
|
248
291
|
return String(value ?? "").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, max);
|
|
249
292
|
}
|
|
@@ -251,13 +294,13 @@ function validateMotionCapture(payload) {
|
|
|
251
294
|
if (!payload || payload.schema !== "ks-epk-motion-recording/v1") fail("ks_motion_schema", "Unsupported capture schema");
|
|
252
295
|
if (!Array.isArray(payload.samples) || payload.samples.length < 1 || payload.samples.length > 500) fail("ks_motion_samples", "A capture must contain 1..500 samples");
|
|
253
296
|
const samples = payload.samples.map((sample) => {
|
|
254
|
-
if (!sample || !
|
|
297
|
+
if (!sample || !numeric2(sample.milliseconds, 0, 12e4) || !numeric2(sample.beta, -180, 180) || !numeric2(sample.gamma, -90, 90)) fail("ks_motion_invalid_samples", "Invalid motion sample");
|
|
255
298
|
return {
|
|
256
299
|
milliseconds: sample.milliseconds,
|
|
257
300
|
beta: sample.beta,
|
|
258
301
|
gamma: sample.gamma,
|
|
259
|
-
alpha: sample.alpha === null || sample.alpha === void 0 ? null :
|
|
260
|
-
screenAngle:
|
|
302
|
+
alpha: sample.alpha === null || sample.alpha === void 0 ? null : numeric2(sample.alpha, 0, 360) ? sample.alpha : null,
|
|
303
|
+
screenAngle: numeric2(sample.screenAngle ?? 0, -360, 360) ? sample.screenAngle || 0 : null,
|
|
261
304
|
...Object.fromEntries(["absolute", "nativeTrusted", "simulatedTrusted", "fieldParity", "dispatchParity", "mapperParity"].map((k) => [k, !!sample[k]]))
|
|
262
305
|
};
|
|
263
306
|
});
|
|
@@ -266,7 +309,7 @@ function validateMotionCapture(payload) {
|
|
|
266
309
|
capturedAt: boundedText(payload.capturedAt, 100),
|
|
267
310
|
userAgent: boundedText(payload.userAgent),
|
|
268
311
|
samples,
|
|
269
|
-
screenAngle:
|
|
312
|
+
screenAngle: numeric2(payload.screenAngle ?? 0, -360, 360) ? payload.screenAngle || 0 : null,
|
|
270
313
|
storedSampleCount: samples.length,
|
|
271
314
|
sampleStrategy: boundedText(payload.sampleStrategy || "legacy", 80).toLowerCase().replace(/[^a-z0-9_-]/g, ""),
|
|
272
315
|
fullWindowObserved: !!payload.fullWindowObserved,
|
|
@@ -278,7 +321,7 @@ function validateMotionCapture(payload) {
|
|
|
278
321
|
validPairs: Number.isFinite(Number(v.validPairs)) ? Math.abs(Math.trunc(Number(v.validPairs))) : null
|
|
279
322
|
}))
|
|
280
323
|
};
|
|
281
|
-
for (const key of ["targetDurationMilliseconds", "captureDurationMilliseconds", "sampledDurationMilliseconds", "storageIntervalMilliseconds"]) clean[key] =
|
|
324
|
+
for (const key of ["targetDurationMilliseconds", "captureDurationMilliseconds", "sampledDurationMilliseconds", "storageIntervalMilliseconds"]) clean[key] = numeric2(payload[key] ?? 0, 0, 12e4) ? payload[key] || 0 : null;
|
|
282
325
|
for (const key of ["processedPairCount", "failureCount"]) clean[key] = Math.min(1e5, Math.abs(Math.trunc(Number(payload[key]) || (key === "processedPairCount" ? samples.length : clean.failures.length))));
|
|
283
326
|
if (Buffer.byteLength(JSON.stringify(clean)) > 262144) fail("ks_motion_size", "Capture too large", 413);
|
|
284
327
|
return clean;
|