@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/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 Number(audio.currentTime) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 60;
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", rawConfig.bpmSource === "explicit" ? "explicit" : "fallback");
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", rawConfig.bpmSource === "explicit" ? "explicit" : "fallback");
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: rawConfig.bpmSource === "explicit" ? "explicit" : "fallback",
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,
@@ -1065,7 +1101,7 @@ function install2(__dmScope) {
1065
1101
  return;
1066
1102
  var instances = /* @__PURE__ */ new Map();
1067
1103
  var serial2 = 0;
1068
- function finite2(value, fallback) {
1104
+ function finite3(value, fallback) {
1069
1105
  value = Number(value);
1070
1106
  return Number.isFinite(value) ? value : fallback;
1071
1107
  }
@@ -1119,8 +1155,8 @@ function install2(__dmScope) {
1119
1155
  rect = bounds.getBoundingClientRect();
1120
1156
  }
1121
1157
  function apply(x, y, reason) {
1122
- latestX = clamp(finite2(x, 0), -1, 1);
1123
- latestY = clamp(finite2(y, 0), -1, 1);
1158
+ latestX = clamp(finite3(x, 0), -1, 1);
1159
+ latestY = clamp(finite3(y, 0), -1, 1);
1124
1160
  __dmScope.setStyle(state.root.style, "--dance-moves-x", latestX.toFixed(4));
1125
1161
  __dmScope.setStyle(state.root.style, "--dance-moves-y", latestY.toFixed(4));
1126
1162
  __dmScope.dataset(state.root, "danceMovesPointer", reason || "active");
@@ -1211,13 +1247,14 @@ function install2(__dmScope) {
1211
1247
  var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1212
1248
  if (!audio)
1213
1249
  throw new Error("DanceMovesEffects pulse requires an audio element");
1214
- var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
1250
+ var ticks = Math.max(1, Math.round(finite3(options.ticks, 16)));
1215
1251
  var className = String(options.className || "dance-moves-playing");
1216
1252
  var prefix = String(options.propertyPrefix || "--dance-moves-pulse").replace(/-+$/, "");
1217
1253
  function sync() {
1218
- var rate = Math.max(0.01, finite2(audio.playbackRate, 1));
1254
+ var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
1219
1255
  var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
1220
- var phase = -(finite2(audio.currentTime, 0) / rate % duration);
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");
@@ -1256,7 +1293,7 @@ function install2(__dmScope) {
1256
1293
  var state = commonState(options, "cue-class");
1257
1294
  var cue = String(options.cue || "*");
1258
1295
  var className = String(options.className || "dance-moves-cue-active");
1259
- var durationTicks = Math.max(1, Math.round(finite2(options.durationTicks, 16)));
1296
+ var durationTicks = Math.max(1, Math.round(finite3(options.durationTicks, 16)));
1260
1297
  var timer = 0, count = 0;
1261
1298
  function clear(reason) {
1262
1299
  if (timer)
@@ -1312,16 +1349,16 @@ function install2(__dmScope) {
1312
1349
  if (!audio)
1313
1350
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1314
1351
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
1315
- var time = finite2(cue && cue.time, NaN);
1352
+ var time = finite3(cue && cue.time, NaN);
1316
1353
  if (!Number.isFinite(time) || time < 0)
1317
1354
  throw new TypeError("DanceMovesEffects cueTimeline cue time must be a non-negative number");
1318
1355
  return Object.freeze({ id: String(cue.id || "cue-" + index), time, end: Number.isFinite(Number(cue.end)) ? Number(cue.end) : null, data: cue.data });
1319
1356
  }).sort(function(a, b) {
1320
1357
  return a.time - b.time;
1321
1358
  });
1322
- var tolerance = Math.max(0, finite2(options.seekLandingTolerance, 0.05));
1323
- var maxCrossingGap = Math.max(0.05, finite2(options.maxCrossingGap, 1.25));
1324
- var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite2(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1359
+ var tolerance = Math.max(0, finite3(options.seekLandingTolerance, 0.05));
1360
+ var maxCrossingGap = Math.max(0.05, finite3(options.maxCrossingGap, 1.25));
1361
+ var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite3(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1325
1362
  function blocked() {
1326
1363
  return document2.hidden || state.reduced.matches || state.forced.matches;
1327
1364
  }
@@ -1331,7 +1368,7 @@ function install2(__dmScope) {
1331
1368
  });
1332
1369
  }
1333
1370
  function publish(reason, extra) {
1334
- var time = finite2(audio.currentTime, 0);
1371
+ var time = finite3(audio.currentTime, 0);
1335
1372
  __dmScope.dataset(state.root, "danceMovesCueTimeline", reason);
1336
1373
  if (typeof options.render === "function")
1337
1374
  options.render({ root: state.root, audio, time, previousTime: previous, reason, playing: !audio.paused && !audio.ended && !blocked(), active: activeAt(time), generation, detail: extra || null });
@@ -1341,10 +1378,10 @@ function install2(__dmScope) {
1341
1378
  return;
1342
1379
  fired.add(cue.id);
1343
1380
  if (typeof options.onCue === "function")
1344
- options.onCue({ root: state.root, audio, cue, time: finite2(audio.currentTime, cue.time), reason, generation });
1381
+ options.onCue({ root: state.root, audio, cue, time: finite3(audio.currentTime, cue.time), reason, generation });
1345
1382
  }
1346
1383
  function rebuild(time, reason, allowLanding) {
1347
- time = Math.max(0, finite2(time, 0));
1384
+ time = Math.max(0, finite3(time, 0));
1348
1385
  generation += 1;
1349
1386
  fired.clear();
1350
1387
  cues.forEach(function(cue) {
@@ -1370,7 +1407,7 @@ function install2(__dmScope) {
1370
1407
  frame = 0;
1371
1408
  if (audio.paused || audio.ended || blocked())
1372
1409
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused");
1373
- var now = Math.max(0, finite2(audio.currentTime, previous));
1410
+ var now = Math.max(0, finite3(audio.currentTime, previous));
1374
1411
  var delta = now - previous;
1375
1412
  if (delta < -1e-3 || delta > maxCrossingGap)
1376
1413
  rebuild(now, "discontinuity", false);
@@ -1379,8 +1416,8 @@ function install2(__dmScope) {
1379
1416
  if (previous < cue.time && now >= cue.time)
1380
1417
  fire(cue, "crossing");
1381
1418
  });
1382
- var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite2(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1383
- previousWall = finite2(wallNow, previousWall);
1419
+ var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite3(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1420
+ previousWall = finite3(wallNow, previousWall);
1384
1421
  previous = now;
1385
1422
  status = "playing";
1386
1423
  publish("frame", { deltaSeconds: wallDelta });
@@ -1389,7 +1426,7 @@ function install2(__dmScope) {
1389
1426
  function start(reason) {
1390
1427
  if (frame || audio.paused || audio.ended || blocked())
1391
1428
  return publish(blocked() ? "inactive" : audio.ended ? "ended" : "paused");
1392
- previous = Math.max(0, finite2(audio.currentTime, 0));
1429
+ previous = Math.max(0, finite3(audio.currentTime, 0));
1393
1430
  previousWall = 0;
1394
1431
  status = reason || "playing";
1395
1432
  publish(status);
@@ -1453,7 +1490,7 @@ function install2(__dmScope) {
1453
1490
  start,
1454
1491
  stop,
1455
1492
  snapshot: function() {
1456
- return { id: state.id, type: "cue-timeline", status, time: finite2(audio.currentTime, 0), previousTime: previous, generation, fired: Array.from(fired), active: activeAt(finite2(audio.currentTime, 0)).map(function(cue) {
1493
+ return { id: state.id, type: "cue-timeline", status, time: finite3(audio.currentTime, 0), previousTime: previous, generation, fired: Array.from(fired), active: activeAt(finite3(audio.currentTime, 0)).map(function(cue) {
1457
1494
  return cue.id;
1458
1495
  }) };
1459
1496
  },
@@ -1476,28 +1513,28 @@ function install2(__dmScope) {
1476
1513
  if (!target)
1477
1514
  throw new Error("DanceMovesEffects spritePlayback requires a target element");
1478
1515
  var frameMap = Array.isArray(options.frameMap) && options.frameMap.length ? options.frameMap.map(function(value) {
1479
- value = Math.floor(finite2(value, -1));
1516
+ value = Math.floor(finite3(value, -1));
1480
1517
  if (value < 0)
1481
1518
  throw new TypeError("DanceMovesEffects spritePlayback frameMap values must be non-negative integers");
1482
1519
  return value;
1483
1520
  }) : null;
1484
- var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite2(options.frameCount, 1)));
1485
- var columns = Math.max(1, Math.round(finite2(options.columns, frameCount)));
1486
- var framesPerRow = clamp(Math.round(finite2(options.framesPerRow, columns)), 1, columns);
1487
- var columnOffset = clamp(Math.round(finite2(options.columnOffset, 0)), 0, columns - framesPerRow);
1488
- var rowOffset = Math.max(0, Math.round(finite2(options.rowOffset, 0)));
1489
- var cycleTicks = Math.max(1, Math.round(finite2(options.cycleTicks, 32)));
1490
- var phaseOffsetSeconds = finite2(options.phaseOffsetSeconds, 0);
1491
- var staticFrame = clamp(Math.round(finite2(options.staticFrame, 0)), 0, frameCount - 1);
1521
+ var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite3(options.frameCount, 1)));
1522
+ var columns = Math.max(1, Math.round(finite3(options.columns, frameCount)));
1523
+ var framesPerRow = clamp(Math.round(finite3(options.framesPerRow, columns)), 1, columns);
1524
+ var columnOffset = clamp(Math.round(finite3(options.columnOffset, 0)), 0, columns - framesPerRow);
1525
+ var rowOffset = Math.max(0, Math.round(finite3(options.rowOffset, 0)));
1526
+ var cycleTicks = Math.max(1, Math.round(finite3(options.cycleTicks, 32)));
1527
+ var phaseOffsetSeconds = finite3(options.phaseOffsetSeconds, 0);
1528
+ var staticFrame = clamp(Math.round(finite3(options.staticFrame, 0)), 0, frameCount - 1);
1492
1529
  var prefix = String(options.propertyPrefix || "--dance-moves-sprite").replace(/-+$/, "");
1493
1530
  if (prefix.slice(0, 2) !== "--")
1494
1531
  prefix = "--" + prefix.replace(/^-+/, "");
1495
1532
  var tiers = Array.isArray(options.qualityTiers) && options.qualityTiers.length ? options.qualityTiers.map(String) : ["full", "constrained", "minimal"];
1496
1533
  var suppliedFps = Array.isArray(options.qualityFramesPerSecond) ? options.qualityFramesPerSecond : [60, 30, 15];
1497
1534
  var qualityFps = tiers.map(function(_, index) {
1498
- return Math.max(1, finite2(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1535
+ return Math.max(1, finite3(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1499
1536
  });
1500
- var qualityIndex = clamp(Math.round(finite2(options.initialQuality, 0)), 0, tiers.length - 1);
1537
+ var qualityIndex = clamp(Math.round(finite3(options.initialQuality, 0)), 0, tiers.length - 1);
1501
1538
  var frame = 0, status = "idle", lastWall = -Infinity, renders = 0, latest = null;
1502
1539
  function blocked() {
1503
1540
  return document2.hidden || state.reduced.matches || state.forced.matches;
@@ -1507,9 +1544,10 @@ function install2(__dmScope) {
1507
1544
  return { sourceFrame, column: sourceFrame % columns, row: Math.floor(sourceFrame / columns) };
1508
1545
  }
1509
1546
  function calculate(reason, staticFallback) {
1510
- var cycleSeconds = Math.max(1e-3, finite2(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1511
- var time = Math.max(0, finite2(audio.currentTime, 0));
1512
- var phase = ((time - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
1547
+ var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1548
+ var time = Math.max(0, finite3(audio.currentTime, 0));
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 {
@@ -1563,8 +1601,8 @@ function install2(__dmScope) {
1563
1601
  if (audio.paused || audio.ended || blocked())
1564
1602
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused", audio.ended || blocked());
1565
1603
  var minimumInterval = 1e3 / qualityFps[qualityIndex];
1566
- if (!Number.isFinite(lastWall) || finite2(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1567
- lastWall = finite2(wallNow, lastWall);
1604
+ if (!Number.isFinite(lastWall) || finite3(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1605
+ lastWall = finite3(wallNow, lastWall);
1568
1606
  status = "playing";
1569
1607
  publish("frame", false);
1570
1608
  }
@@ -1595,7 +1633,7 @@ function install2(__dmScope) {
1595
1633
  }
1596
1634
  function setQuality(value, reason) {
1597
1635
  var named = typeof value === "string" ? tiers.indexOf(value) : -1;
1598
- qualityIndex = named >= 0 ? named : clamp(Math.round(finite2(value, qualityIndex)), 0, tiers.length - 1);
1636
+ qualityIndex = named >= 0 ? named : clamp(Math.round(finite3(value, qualityIndex)), 0, tiers.length - 1);
1599
1637
  lastWall = -Infinity;
1600
1638
  publish(reason || "quality-change", audio.ended || blocked());
1601
1639
  }
@@ -1649,7 +1687,7 @@ function install2(__dmScope) {
1649
1687
  },
1650
1688
  setQuality,
1651
1689
  snapshot: function() {
1652
- return { id: state.id, type: "sprite-playback", status, time: finite2(audio.currentTime, 0), frame: latest ? latest.frame : null, localFrame: latest ? latest.localFrame : null, column: latest ? latest.column : null, row: latest ? latest.row : null, phase: latest ? latest.phase : null, cycleTicks, cycleSeconds: latest ? latest.cycleSeconds : null, qualityTier: tiers[qualityIndex], qualityIndex, renders, running: Boolean(frame) };
1690
+ return { id: state.id, type: "sprite-playback", status, time: finite3(audio.currentTime, 0), frame: latest ? latest.frame : null, localFrame: latest ? latest.localFrame : null, column: latest ? latest.column : null, row: latest ? latest.row : null, phase: latest ? latest.phase : null, cycleTicks, cycleSeconds: latest ? latest.cycleSeconds : null, qualityTier: tiers[qualityIndex], qualityIndex, renders, running: Boolean(frame) };
1653
1691
  },
1654
1692
  teardown: function() {
1655
1693
  stop("teardown", true);
@@ -1716,8 +1754,8 @@ function install2(__dmScope) {
1716
1754
  popover.appendChild(viewport);
1717
1755
  __dmScope.dataset(popover, "danceMovesLyricStage", "ready");
1718
1756
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", "0");
1719
- var travelTicks = Math.max(1, Math.round(finite2(options.travelTicks, 32)));
1720
- var defaultCueTicks = Math.max(1, Math.round(finite2(options.cueDurationTicks, 32)));
1757
+ var travelTicks = Math.max(1, Math.round(finite3(options.travelTicks, 32)));
1758
+ var defaultCueTicks = Math.max(1, Math.round(finite3(options.cueDurationTicks, 32)));
1721
1759
  var latest = null, phase = "idle", progress = 0, frame = 0, wakeTimer = 0, cueTimer = 0, cueCleanup = null, cueCount = 0;
1722
1760
  var lyricEffects = [];
1723
1761
  var slots = Object.freeze({ previous, current, next, viewport, track });
@@ -1766,7 +1804,7 @@ function install2(__dmScope) {
1766
1804
  publish(reason || value);
1767
1805
  }
1768
1806
  function setProgress(value) {
1769
- progress = clamp(finite2(value, 0), 0, 1);
1807
+ progress = clamp(finite3(value, 0), 0, 1);
1770
1808
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", progress.toFixed(5));
1771
1809
  }
1772
1810
  function copyLyrics(detail) {
@@ -1783,7 +1821,7 @@ function install2(__dmScope) {
1783
1821
  var cleanup = typeof result === "function" ? result : typeof result.cleanup === "function" ? result.cleanup : null;
1784
1822
  if (!cleanup)
1785
1823
  return;
1786
- var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite2(result.durationTicks, 64))) : 64;
1824
+ var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite3(result.durationTicks, 64))) : 64;
1787
1825
  var record = { cleanup, timer: 0 };
1788
1826
  record.timer = window2.setTimeout(function() {
1789
1827
  lyricEffects = lyricEffects.filter(function(item) {
@@ -1809,7 +1847,7 @@ function install2(__dmScope) {
1809
1847
  setProgress(0);
1810
1848
  return setPhase("waiting", "no-next-line");
1811
1849
  }
1812
- var now = Math.max(0, finite2(audio.currentTime, 0));
1850
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1813
1851
  if (now < windowRange.start)
1814
1852
  return reconcile("frame-before-window");
1815
1853
  if (now >= windowRange.end) {
@@ -1839,7 +1877,7 @@ function install2(__dmScope) {
1839
1877
  setProgress(0);
1840
1878
  return setPhase("waiting", reason || "no-next-line");
1841
1879
  }
1842
- var now = Math.max(0, finite2(audio.currentTime, 0));
1880
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1843
1881
  if (now >= windowRange.end) {
1844
1882
  setProgress(1);
1845
1883
  return setPhase("arrived", reason || "arrived");
@@ -1854,7 +1892,7 @@ function install2(__dmScope) {
1854
1892
  wakeTimer = window2.setTimeout(function() {
1855
1893
  wakeTimer = 0;
1856
1894
  tick();
1857
- }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite2(audio.playbackRate, 1))));
1895
+ }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite3(audio.playbackRate, 1))));
1858
1896
  }
1859
1897
  function onLyric(detail) {
1860
1898
  var previousDetail = latest;
@@ -1909,7 +1947,7 @@ function install2(__dmScope) {
1909
1947
  else if (result && typeof result === "object") {
1910
1948
  if (typeof result.cleanup === "function")
1911
1949
  cueCleanup = result.cleanup;
1912
- durationTicks = Math.max(1, Math.round(finite2(result.durationTicks, defaultCueTicks)));
1950
+ durationTicks = Math.max(1, Math.round(finite3(result.durationTicks, defaultCueTicks)));
1913
1951
  }
1914
1952
  cueTimer = window2.setTimeout(function() {
1915
1953
  clearCue("complete");
@@ -1981,16 +2019,16 @@ function install2(__dmScope) {
1981
2019
  var hazardSources = Array.isArray(options.hazardSources) ? options.hazardSources.filter(Boolean).map(String) : [];
1982
2020
  if (!shipSources.length || !hazardSources.length)
1983
2021
  throw new Error("DanceMovesEffects cooperativeArena requires ship and hazard sources");
1984
- var shipCount = clamp(Math.round(finite2(options.shipCount, 3)), 2, 12);
1985
- var hazardCount = clamp(Math.round(finite2(options.hazardCount, 8)), 3, 48);
1986
- var speed = clamp(finite2(options.speed, 1), 0.35, 2.4);
2022
+ var shipCount = clamp(Math.round(finite3(options.shipCount, 3)), 2, 12);
2023
+ var hazardCount = clamp(Math.round(finite3(options.hazardCount, 8)), 3, 48);
2024
+ var speed = clamp(finite3(options.speed, 1), 0.35, 2.4);
1987
2025
  var inlineTransform = options.inlineTransform === true;
1988
- var bpm = Math.max(1, finite2(options.bpm, motion.bpm || 120));
1989
- var shotIntervalTicks = clamp(Math.round(finite2(options.shotIntervalTicks, 32)), 4, 256);
2026
+ var bpm = Math.max(1, finite3(options.bpm, motion.bpm || 120));
2027
+ var shotIntervalTicks = clamp(Math.round(finite3(options.shotIntervalTicks, 32)), 4, 256);
1990
2028
  var shotIntervalSeconds = motion.durationMilliseconds ? motion.durationMilliseconds(shotIntervalTicks, bpm) / 1e3 : shotIntervalTicks * 60 / (bpm * 16);
1991
- var burstDurationSeconds = clamp(finite2(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1992
- var shipHitboxScale = clamp(finite2(options.shipHitboxScale, 1), 0.4, 1);
1993
- var hazardHitboxScale = clamp(finite2(options.hazardHitboxScale, 1), 0.4, 1);
2029
+ var burstDurationSeconds = clamp(finite3(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
2030
+ var shipHitboxScale = clamp(finite3(options.shipHitboxScale, 1), 0.4, 1);
2031
+ var hazardHitboxScale = clamp(finite3(options.hazardHitboxScale, 1), 0.4, 1);
1994
2032
  var removers = state.removers, ships = [], hazards = [], shots = [], bursts = [], burstTimers = [];
1995
2033
  var activeShips = shipCount, activeHazards = hazardCount;
1996
2034
  var width = 1, height = 1, frame = 0, last = 0, elapsed = 0, nextShot = 0, hits = 0;
@@ -2322,7 +2360,7 @@ function install2(__dmScope) {
2322
2360
  reconcile(reason || "manual");
2323
2361
  },
2324
2362
  setDensity: function(nextShips, nextHazards) {
2325
- var shipsNext = clamp(Math.round(finite2(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite2(nextHazards, activeHazards)), 3, hazardCount);
2363
+ var shipsNext = clamp(Math.round(finite3(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite3(nextHazards, activeHazards)), 3, hazardCount);
2326
2364
  if (shipsNext === activeShips && hazardsNext === activeHazards)
2327
2365
  return;
2328
2366
  activeShips = shipsNext;
@@ -2365,12 +2403,12 @@ function install2(__dmScope) {
2365
2403
  options = options || {};
2366
2404
  var state = commonState(options, "quality");
2367
2405
  var tiers = Array.isArray(options.tiers) && options.tiers.length ? options.tiers.map(String) : ["full", "constrained", "minimal"];
2368
- var index = clamp(Math.round(finite2(options.initial, 0)), 0, tiers.length - 1);
2369
- var sampleMs = Math.max(800, finite2(options.sampleMilliseconds, 1600));
2370
- var poorWindows = Math.max(2, Math.round(finite2(options.poorWindows, 2)));
2371
- var healthyWindows = Math.max(3, Math.round(finite2(options.healthyWindows, 5)));
2372
- var downgradeRatio = clamp(finite2(options.downgradeRatio, 0.72), 0.2, 0.95);
2373
- var recoveryRatio = clamp(finite2(options.recoveryRatio, 0.9), downgradeRatio, 1);
2406
+ var index = clamp(Math.round(finite3(options.initial, 0)), 0, tiers.length - 1);
2407
+ var sampleMs = Math.max(800, finite3(options.sampleMilliseconds, 1600));
2408
+ var poorWindows = Math.max(2, Math.round(finite3(options.poorWindows, 2)));
2409
+ var healthyWindows = Math.max(3, Math.round(finite3(options.healthyWindows, 5)));
2410
+ var downgradeRatio = clamp(finite3(options.downgradeRatio, 0.72), 0.2, 0.95);
2411
+ var recoveryRatio = clamp(finite3(options.recoveryRatio, 0.9), downgradeRatio, 1);
2374
2412
  var frame = 0, frames = 0, start = 0, reference = 0, poor = 0, healthy = 0, lastShift = 0, running = true;
2375
2413
  var preferenceLimited = false, tierBeforePreference = index;
2376
2414
  function publish(reason, sample) {
@@ -2414,12 +2452,12 @@ function install2(__dmScope) {
2414
2452
  poor = 0;
2415
2453
  healthy = 0;
2416
2454
  }
2417
- if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite2(options.downgradeCooldownMilliseconds, 5e3)) {
2455
+ if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite3(options.downgradeCooldownMilliseconds, 5e3)) {
2418
2456
  index += 1;
2419
2457
  poor = 0;
2420
2458
  lastShift = now;
2421
2459
  publish("sustained-low-fps", { fps });
2422
- } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite2(options.recoveryCooldownMilliseconds, 12e3)) {
2460
+ } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite3(options.recoveryCooldownMilliseconds, 12e3)) {
2423
2461
  index -= 1;
2424
2462
  healthy = 0;
2425
2463
  lastShift = now;
@@ -2458,7 +2496,7 @@ function install2(__dmScope) {
2458
2496
  id: state.id,
2459
2497
  type: "quality",
2460
2498
  setTier: function(value, reason) {
2461
- index = clamp(Math.round(finite2(value, index)), 0, tiers.length - 1);
2499
+ index = clamp(Math.round(finite3(value, index)), 0, tiers.length - 1);
2462
2500
  publish(reason || "manual");
2463
2501
  },
2464
2502
  snapshot: function() {
@@ -4928,8 +4966,9 @@ function install9(__dmScope) {
4928
4966
  anchor = 0;
4929
4967
  lastClock = mode;
4930
4968
  const tempo = bpm();
4931
- const beats = useAudio ? Math.max(0, Number.isFinite(audio.currentTime) ? audio.currentTime : 0) * tempo / 60 : motion.currentTick({ clock: "page" }) / 16;
4932
- return { beats, mode, tempo };
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.beats,
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 bpm = explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
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: explicit && input.bpmSource !== "fallback" ? "explicit" : "fallback",
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 numeric = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
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 = numeric(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
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);
@@ -6436,6 +6520,184 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
6436
6520
  });
6437
6521
  }
6438
6522
 
6523
+ // .build/modules/recordings.mjs
6524
+ var WASM = "AGFzbQEAAAABDwJgAAF/YAZ/f39/f38BfAMDAgABBQUBAQKAQAYIAX8BQYCIBAsHKgMGbWVtb3J5AgAKdGFibGVfYmFzZQAAEHNhbXBsZV9yZWNvcmRpbmcAAQpvAggAQYCIhIAAC2QCAXwBfkQAAAAAAAD4fyEGAkAgAEUNACABQQFIDQAgAkEBSA0AIAVBAEgNACAFIAJPDQAgBKwgA6x9IgdCAFMNACAHIAGtWg0AIAAgB6cgAmxBA3RqIAVBA3RqKwMAIQYLIAYL";
6525
+ var MAX_VALUES = 8388608;
6526
+ var MAX_BYTES = 64 * 1024 * 1024;
6527
+ var modulePromise;
6528
+ var finite2 = (n, label) => {
6529
+ if (typeof n !== "number" || !Number.isFinite(n)) throw new TypeError(label + " must be finite");
6530
+ return n;
6531
+ };
6532
+ function validateRecording(input) {
6533
+ if (!input || input.format !== "dancerudiments.recording" || input.schema_version !== 1 || input.pips_per_beat !== 64 || input.sample_dtype !== "float64") throw new TypeError("Expected dancerudiments.recording v1, float64, 64 pips per beat");
6534
+ if (typeof input.name !== "string" || !input.name.length || input.name.length > 128) throw new TypeError("Recording name is required");
6535
+ const dimensions = input.dimensions;
6536
+ if (!Array.isArray(dimensions) || !dimensions.length || dimensions.length > 64 || new Set(dimensions).size !== dimensions.length || dimensions.some((d) => typeof d !== "string" || !/^[a-z][a-z0-9_]{0,127}$/.test(d))) throw new TypeError("Recording requires 1..64 unique named dimensions");
6537
+ const c = input.clock;
6538
+ if (!c || finite2(c.bpm, "Recording BPM") <= 0 || !["confirmed", "estimated"].includes(c.bpm_status) || !["unverified", "estimated", "supplied"].includes(c.phase_status) || typeof c.bpm_source !== "string" || !c.bpm_source.trim()) throw new TypeError("Recording clock requires BPM and provenance");
6539
+ finite2(c.beat_zero_seconds, "Beat zero");
6540
+ if (!Number.isInteger(c.start_pip) || c.start_pip < -2147483648) throw new RangeError("Invalid recording start pip");
6541
+ const rows = input.samples;
6542
+ if (!Array.isArray(rows) || !rows.length || rows.length * dimensions.length > MAX_VALUES || c.start_pip + rows.length > 2147483647) throw new RangeError("Recording exceeds bounded sample limits");
6543
+ const values = new Float64Array(rows.length * dimensions.length);
6544
+ rows.forEach((row, i) => {
6545
+ if (!Array.isArray(row) || row.length !== dimensions.length) throw new TypeError("Recording sample dimensions differ");
6546
+ row.forEach((v, j) => {
6547
+ finite2(v, "Sample");
6548
+ if (Math.abs(v) > 1) throw new RangeError("Recording samples must be in [-1,1]");
6549
+ values[i * dimensions.length + j] = v;
6550
+ });
6551
+ });
6552
+ return Object.freeze({ name: input.name, dimensions: Object.freeze([...dimensions]), count: rows.length, clock: Object.freeze({ ...c }), values });
6553
+ }
6554
+ async function samplerFromData(data) {
6555
+ if (!modulePromise) modulePromise = WebAssembly.compile(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))).catch((error) => {
6556
+ modulePromise = null;
6557
+ throw error;
6558
+ });
6559
+ const api = (await WebAssembly.instantiate(await modulePromise, {})).exports;
6560
+ const base = api.table_base(), pages = Math.ceil((base + data.values.byteLength - api.memory.buffer.byteLength) / 65536);
6561
+ if (pages > 0) api.memory.grow(pages);
6562
+ new Float64Array(api.memory.buffer, base, data.values.length).set(data.values);
6563
+ return Object.freeze({ ...data, sample(pip) {
6564
+ if (!Number.isInteger(pip) || pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count) throw new RangeError("Position is outside the non-looping recording");
6565
+ return data.dimensions.map((_, axis) => api.sample_recording(base, data.count, data.dimensions.length, data.clock.start_pip, pip, axis));
6566
+ } });
6567
+ }
6568
+ function createRecordingService(scope, rudiments, config) {
6569
+ const handles = /* @__PURE__ */ new Map();
6570
+ async function load(url2) {
6571
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6572
+ const response = await scope.window.fetch(url2);
6573
+ if (!response.ok) throw new Error("Recording request failed: " + response.status);
6574
+ if (Number(response.headers?.get("content-length")) > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6575
+ let text;
6576
+ if (response.body?.getReader) {
6577
+ const reader = response.body.getReader(), chunks = [];
6578
+ let size = 0;
6579
+ for (; ; ) {
6580
+ const { done, value } = await reader.read();
6581
+ if (done) break;
6582
+ size += value.byteLength;
6583
+ if (size > MAX_BYTES) {
6584
+ await reader.cancel();
6585
+ throw new RangeError("Recording exceeds 64 MiB");
6586
+ }
6587
+ chunks.push(value);
6588
+ }
6589
+ const bytes = new Uint8Array(size);
6590
+ let offset = 0;
6591
+ for (const chunk of chunks) {
6592
+ bytes.set(chunk, offset);
6593
+ offset += chunk.length;
6594
+ }
6595
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6596
+ } else {
6597
+ text = await response.text();
6598
+ if (new TextEncoder().encode(text).length > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6599
+ }
6600
+ const input = JSON.parse(text);
6601
+ validateRecording(input);
6602
+ return input;
6603
+ }
6604
+ function animate(options = {}) {
6605
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6606
+ if (typeof options.id !== "string" || !options.id.trim() || handles.has(options.id)) throw new TypeError("Recording controller requires a unique id");
6607
+ if (typeof options.render !== "function") throw new TypeError("Recording render callback is required");
6608
+ if (!options.audio) throw new TypeError("Recording playback requires an audio/player source");
6609
+ if (["rate", "phasePips", "clock", "bpm"].some((k) => Object.hasOwn(options, k))) throw new TypeError("Recording clock, rate and origin come from its capture");
6610
+ const data = validateRecording(options.recording);
6611
+ if (Math.abs(data.clock.bpm - config.bpm) > 1e-9) throw new RangeError("Recording BPM does not match the authoritative page BPM");
6612
+ let inner = null, sampler = null, destroyed = false, enabled = options.enabled !== false, paused = false, status = "loading", error = null, last = null;
6613
+ const output = (frame, values, pip, reason) => {
6614
+ if (destroyed) return;
6615
+ last = { ...frame, rudiment: null, recording: data.name, dimensions: data.dimensions, values, position: { x: values[0] || 0, y: values[1] || 0, z: values[2] || 0 }, pip, positionPips: pip, loopProgress: null, reason };
6616
+ options.render(last);
6617
+ };
6618
+ const handle = {
6619
+ id: options.id,
6620
+ ready: null,
6621
+ setEnabled(value) {
6622
+ if (typeof value !== "boolean") throw new TypeError("enabled must be boolean");
6623
+ enabled = value;
6624
+ inner?.setEnabled(value);
6625
+ },
6626
+ pause() {
6627
+ paused = true;
6628
+ inner?.pause();
6629
+ },
6630
+ resume() {
6631
+ paused = false;
6632
+ inner?.resume();
6633
+ },
6634
+ refresh() {
6635
+ inner?.refresh();
6636
+ },
6637
+ destroy() {
6638
+ if (destroyed) return;
6639
+ inner?.destroy();
6640
+ inner = null;
6641
+ sampler = null;
6642
+ destroyed = true;
6643
+ status = "destroyed";
6644
+ handles.delete(options.id);
6645
+ },
6646
+ snapshot() {
6647
+ const state = inner?.snapshot() ?? null;
6648
+ return { id: options.id, type: "recording", recording: data.name, status: !destroyed && ["hidden", "offscreen", "paused", "seeking"].includes(state?.status) ? state.status : status, error, destroyed, enabled, paused, pip: last?.pip ?? null, dimensions: data.dimensions, count: data.count, clock: data.clock, inner: state };
6649
+ }
6650
+ };
6651
+ handle.teardown = handle.destroy;
6652
+ handles.set(options.id, handle);
6653
+ handle.ready = samplerFromData(data).then(async (native) => {
6654
+ if (destroyed || scope.disposed) return handle;
6655
+ sampler = native;
6656
+ inner = rudiments.animate({
6657
+ id: options.id,
6658
+ root: options.root,
6659
+ target: options.target,
6660
+ audio: options.audio,
6661
+ clock: "audio",
6662
+ rudiment: "circle",
6663
+ rate: 1,
6664
+ phasePips: 0,
6665
+ amplitude: 0,
6666
+ css: false,
6667
+ renderEveryFrame: true,
6668
+ offscreen: options.offscreen !== false,
6669
+ enabled,
6670
+ render(frame) {
6671
+ if (destroyed) return;
6672
+ const neutral = ["disabled", "reduced-motion", "forced-colors"].includes(frame.reason), precise = (frame.sourceBeats - data.clock.beat_zero_seconds * data.clock.bpm / 60) * 64;
6673
+ const pip = Math.floor(precise + 1e-8), outside = pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count;
6674
+ status = neutral ? frame.reason : outside ? "outside-recording" : frame.reason;
6675
+ const values = neutral || outside ? data.dimensions.map(() => 0) : sampler.sample(pip);
6676
+ output(frame, values, pip, status);
6677
+ }
6678
+ });
6679
+ if (paused) inner.pause();
6680
+ await inner.ready;
6681
+ return handle;
6682
+ }).catch((failure) => {
6683
+ error = String(failure.message || failure);
6684
+ status = "error";
6685
+ inner?.destroy();
6686
+ inner = null;
6687
+ sampler = null;
6688
+ if (handles.get(options.id) === handle) handles.delete(options.id);
6689
+ throw failure;
6690
+ });
6691
+ return handle;
6692
+ }
6693
+ scope.onDispose(() => {
6694
+ for (const handle of [...handles.values()]) handle.destroy();
6695
+ });
6696
+ return Object.freeze({ load, animate, get: (id) => handles.get(id) || null, destroyAll: () => {
6697
+ for (const h of [...handles.values()]) h.destroy();
6698
+ }, snapshot: () => ({ native: "DanceRudiments 0.3.0 recording C++/WASM", instances: [...handles.values()].map((h) => h.snapshot()) }) });
6699
+ }
6700
+
6439
6701
  // .build/modules/child-playback.mjs
6440
6702
  var PROTOCOL = "dancemoves.child-playback.v1";
6441
6703
  var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
@@ -6860,7 +7122,7 @@ function installChildPlayback(scope, api, host) {
6860
7122
  }
6861
7123
 
6862
7124
  // .build/modules/version.mjs
6863
- var VERSION = "3.1.23";
7125
+ var VERSION = "3.3.0";
6864
7126
 
6865
7127
  // .build/modules/index.mjs
6866
7128
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -6913,6 +7175,7 @@ function createDanceMoves(options = {}) {
6913
7175
  config,
6914
7176
  effects: w.DanceMovesEffects,
6915
7177
  rudiments: w.DanceMovesRudiments,
7178
+ recordings: createRecordingService(scope, w.DanceMovesRudiments, config),
6916
7179
  orientationCore: w.KSEpkOrientationCore,
6917
7180
  getOrientation: () => w.__ksEpkOrientationRuntime || null,
6918
7181
  getClay: () => w.DanceMovesClayStars || null,