@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.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 Number(audio.currentTime) * effectiveBpm(settings.bpm || bpm) * TICKS_PER_BEAT / 60;
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", rawConfig.bpmSource === "explicit" ? "explicit" : "fallback");
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", rawConfig.bpmSource === "explicit" ? "explicit" : "fallback");
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: rawConfig.bpmSource === "explicit" ? "explicit" : "fallback",
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,
@@ -1032,7 +1067,7 @@ function install2(__dmScope) {
1032
1067
  return;
1033
1068
  var instances = /* @__PURE__ */ new Map();
1034
1069
  var serial2 = 0;
1035
- function finite2(value, fallback) {
1070
+ function finite3(value, fallback) {
1036
1071
  value = Number(value);
1037
1072
  return Number.isFinite(value) ? value : fallback;
1038
1073
  }
@@ -1086,8 +1121,8 @@ function install2(__dmScope) {
1086
1121
  rect = bounds.getBoundingClientRect();
1087
1122
  }
1088
1123
  function apply(x, y, reason) {
1089
- latestX = clamp(finite2(x, 0), -1, 1);
1090
- latestY = clamp(finite2(y, 0), -1, 1);
1124
+ latestX = clamp(finite3(x, 0), -1, 1);
1125
+ latestY = clamp(finite3(y, 0), -1, 1);
1091
1126
  __dmScope.setStyle(state.root.style, "--dance-moves-x", latestX.toFixed(4));
1092
1127
  __dmScope.setStyle(state.root.style, "--dance-moves-y", latestY.toFixed(4));
1093
1128
  __dmScope.dataset(state.root, "danceMovesPointer", reason || "active");
@@ -1178,13 +1213,14 @@ function install2(__dmScope) {
1178
1213
  var audio = playbackElement(options.audio, state.root.querySelector("audio"));
1179
1214
  if (!audio)
1180
1215
  throw new Error("DanceMovesEffects pulse requires an audio element");
1181
- var ticks = Math.max(1, Math.round(finite2(options.ticks, 16)));
1216
+ var ticks = Math.max(1, Math.round(finite3(options.ticks, 16)));
1182
1217
  var className = String(options.className || "dance-moves-playing");
1183
1218
  var prefix = String(options.propertyPrefix || "--dance-moves-pulse").replace(/-+$/, "");
1184
1219
  function sync() {
1185
- var rate = Math.max(0.01, finite2(audio.playbackRate, 1));
1220
+ var rate = Math.max(0.01, finite3(audio.playbackRate, 1));
1186
1221
  var duration = motion.durationMilliseconds(ticks) / 1e3 / rate;
1187
- var phase = -(finite2(audio.currentTime, 0) / rate % duration);
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");
@@ -1223,7 +1259,7 @@ function install2(__dmScope) {
1223
1259
  var state = commonState(options, "cue-class");
1224
1260
  var cue = String(options.cue || "*");
1225
1261
  var className = String(options.className || "dance-moves-cue-active");
1226
- var durationTicks = Math.max(1, Math.round(finite2(options.durationTicks, 16)));
1262
+ var durationTicks = Math.max(1, Math.round(finite3(options.durationTicks, 16)));
1227
1263
  var timer = 0, count = 0;
1228
1264
  function clear(reason) {
1229
1265
  if (timer)
@@ -1279,16 +1315,16 @@ function install2(__dmScope) {
1279
1315
  if (!audio)
1280
1316
  throw new Error("DanceMovesEffects cueTimeline requires an audio element");
1281
1317
  var cues = (Array.isArray(options.cues) ? options.cues : []).map(function(cue, index) {
1282
- var time = finite2(cue && cue.time, NaN);
1318
+ var time = finite3(cue && cue.time, NaN);
1283
1319
  if (!Number.isFinite(time) || time < 0)
1284
1320
  throw new TypeError("DanceMovesEffects cueTimeline cue time must be a non-negative number");
1285
1321
  return Object.freeze({ id: String(cue.id || "cue-" + index), time, end: Number.isFinite(Number(cue.end)) ? Number(cue.end) : null, data: cue.data });
1286
1322
  }).sort(function(a, b) {
1287
1323
  return a.time - b.time;
1288
1324
  });
1289
- var tolerance = Math.max(0, finite2(options.seekLandingTolerance, 0.05));
1290
- var maxCrossingGap = Math.max(0.05, finite2(options.maxCrossingGap, 1.25));
1291
- var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite2(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1325
+ var tolerance = Math.max(0, finite3(options.seekLandingTolerance, 0.05));
1326
+ var maxCrossingGap = Math.max(0.05, finite3(options.maxCrossingGap, 1.25));
1327
+ var fired = /* @__PURE__ */ new Set(), frame = 0, previous = finite3(audio.currentTime, 0), status = "idle", generation = 0, previousWall = 0;
1292
1328
  function blocked() {
1293
1329
  return document2.hidden || state.reduced.matches || state.forced.matches;
1294
1330
  }
@@ -1298,7 +1334,7 @@ function install2(__dmScope) {
1298
1334
  });
1299
1335
  }
1300
1336
  function publish(reason, extra) {
1301
- var time = finite2(audio.currentTime, 0);
1337
+ var time = finite3(audio.currentTime, 0);
1302
1338
  __dmScope.dataset(state.root, "danceMovesCueTimeline", reason);
1303
1339
  if (typeof options.render === "function")
1304
1340
  options.render({ root: state.root, audio, time, previousTime: previous, reason, playing: !audio.paused && !audio.ended && !blocked(), active: activeAt(time), generation, detail: extra || null });
@@ -1308,10 +1344,10 @@ function install2(__dmScope) {
1308
1344
  return;
1309
1345
  fired.add(cue.id);
1310
1346
  if (typeof options.onCue === "function")
1311
- options.onCue({ root: state.root, audio, cue, time: finite2(audio.currentTime, cue.time), reason, generation });
1347
+ options.onCue({ root: state.root, audio, cue, time: finite3(audio.currentTime, cue.time), reason, generation });
1312
1348
  }
1313
1349
  function rebuild(time, reason, allowLanding) {
1314
- time = Math.max(0, finite2(time, 0));
1350
+ time = Math.max(0, finite3(time, 0));
1315
1351
  generation += 1;
1316
1352
  fired.clear();
1317
1353
  cues.forEach(function(cue) {
@@ -1337,7 +1373,7 @@ function install2(__dmScope) {
1337
1373
  frame = 0;
1338
1374
  if (audio.paused || audio.ended || blocked())
1339
1375
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused");
1340
- var now = Math.max(0, finite2(audio.currentTime, previous));
1376
+ var now = Math.max(0, finite3(audio.currentTime, previous));
1341
1377
  var delta = now - previous;
1342
1378
  if (delta < -1e-3 || delta > maxCrossingGap)
1343
1379
  rebuild(now, "discontinuity", false);
@@ -1346,8 +1382,8 @@ function install2(__dmScope) {
1346
1382
  if (previous < cue.time && now >= cue.time)
1347
1383
  fire(cue, "crossing");
1348
1384
  });
1349
- var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite2(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1350
- previousWall = finite2(wallNow, previousWall);
1385
+ var wallDelta = previousWall ? Math.min(0.25, Math.max(0, (finite3(wallNow, previousWall) - previousWall) / 1e3)) : 0;
1386
+ previousWall = finite3(wallNow, previousWall);
1351
1387
  previous = now;
1352
1388
  status = "playing";
1353
1389
  publish("frame", { deltaSeconds: wallDelta });
@@ -1356,7 +1392,7 @@ function install2(__dmScope) {
1356
1392
  function start(reason) {
1357
1393
  if (frame || audio.paused || audio.ended || blocked())
1358
1394
  return publish(blocked() ? "inactive" : audio.ended ? "ended" : "paused");
1359
- previous = Math.max(0, finite2(audio.currentTime, 0));
1395
+ previous = Math.max(0, finite3(audio.currentTime, 0));
1360
1396
  previousWall = 0;
1361
1397
  status = reason || "playing";
1362
1398
  publish(status);
@@ -1420,7 +1456,7 @@ function install2(__dmScope) {
1420
1456
  start,
1421
1457
  stop,
1422
1458
  snapshot: function() {
1423
- 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) {
1459
+ 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) {
1424
1460
  return cue.id;
1425
1461
  }) };
1426
1462
  },
@@ -1443,28 +1479,28 @@ function install2(__dmScope) {
1443
1479
  if (!target)
1444
1480
  throw new Error("DanceMovesEffects spritePlayback requires a target element");
1445
1481
  var frameMap = Array.isArray(options.frameMap) && options.frameMap.length ? options.frameMap.map(function(value) {
1446
- value = Math.floor(finite2(value, -1));
1482
+ value = Math.floor(finite3(value, -1));
1447
1483
  if (value < 0)
1448
1484
  throw new TypeError("DanceMovesEffects spritePlayback frameMap values must be non-negative integers");
1449
1485
  return value;
1450
1486
  }) : null;
1451
- var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite2(options.frameCount, 1)));
1452
- var columns = Math.max(1, Math.round(finite2(options.columns, frameCount)));
1453
- var framesPerRow = clamp(Math.round(finite2(options.framesPerRow, columns)), 1, columns);
1454
- var columnOffset = clamp(Math.round(finite2(options.columnOffset, 0)), 0, columns - framesPerRow);
1455
- var rowOffset = Math.max(0, Math.round(finite2(options.rowOffset, 0)));
1456
- var cycleTicks = Math.max(1, Math.round(finite2(options.cycleTicks, 32)));
1457
- var phaseOffsetSeconds = finite2(options.phaseOffsetSeconds, 0);
1458
- var staticFrame = clamp(Math.round(finite2(options.staticFrame, 0)), 0, frameCount - 1);
1487
+ var frameCount = frameMap ? frameMap.length : Math.max(1, Math.round(finite3(options.frameCount, 1)));
1488
+ var columns = Math.max(1, Math.round(finite3(options.columns, frameCount)));
1489
+ var framesPerRow = clamp(Math.round(finite3(options.framesPerRow, columns)), 1, columns);
1490
+ var columnOffset = clamp(Math.round(finite3(options.columnOffset, 0)), 0, columns - framesPerRow);
1491
+ var rowOffset = Math.max(0, Math.round(finite3(options.rowOffset, 0)));
1492
+ var cycleTicks = Math.max(1, Math.round(finite3(options.cycleTicks, 32)));
1493
+ var phaseOffsetSeconds = finite3(options.phaseOffsetSeconds, 0);
1494
+ var staticFrame = clamp(Math.round(finite3(options.staticFrame, 0)), 0, frameCount - 1);
1459
1495
  var prefix = String(options.propertyPrefix || "--dance-moves-sprite").replace(/-+$/, "");
1460
1496
  if (prefix.slice(0, 2) !== "--")
1461
1497
  prefix = "--" + prefix.replace(/^-+/, "");
1462
1498
  var tiers = Array.isArray(options.qualityTiers) && options.qualityTiers.length ? options.qualityTiers.map(String) : ["full", "constrained", "minimal"];
1463
1499
  var suppliedFps = Array.isArray(options.qualityFramesPerSecond) ? options.qualityFramesPerSecond : [60, 30, 15];
1464
1500
  var qualityFps = tiers.map(function(_, index) {
1465
- return Math.max(1, finite2(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1501
+ return Math.max(1, finite3(suppliedFps[index], suppliedFps[suppliedFps.length - 1] || 15));
1466
1502
  });
1467
- var qualityIndex = clamp(Math.round(finite2(options.initialQuality, 0)), 0, tiers.length - 1);
1503
+ var qualityIndex = clamp(Math.round(finite3(options.initialQuality, 0)), 0, tiers.length - 1);
1468
1504
  var frame = 0, status = "idle", lastWall = -Infinity, renders = 0, latest = null;
1469
1505
  function blocked() {
1470
1506
  return document2.hidden || state.reduced.matches || state.forced.matches;
@@ -1474,9 +1510,10 @@ function install2(__dmScope) {
1474
1510
  return { sourceFrame, column: sourceFrame % columns, row: Math.floor(sourceFrame / columns) };
1475
1511
  }
1476
1512
  function calculate(reason, staticFallback) {
1477
- var cycleSeconds = Math.max(1e-3, finite2(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1478
- var time = Math.max(0, finite2(audio.currentTime, 0));
1479
- var phase = ((time - phaseOffsetSeconds) % cycleSeconds + cycleSeconds) % cycleSeconds / cycleSeconds;
1513
+ var cycleSeconds = Math.max(1e-3, finite3(motion.durationMilliseconds(cycleTicks), 1) / 1e3);
1514
+ var time = Math.max(0, finite3(audio.currentTime, 0));
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 {
@@ -1530,8 +1567,8 @@ function install2(__dmScope) {
1530
1567
  if (audio.paused || audio.ended || blocked())
1531
1568
  return stop(audio.ended ? "ended" : blocked() ? "inactive" : "paused", audio.ended || blocked());
1532
1569
  var minimumInterval = 1e3 / qualityFps[qualityIndex];
1533
- if (!Number.isFinite(lastWall) || finite2(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1534
- lastWall = finite2(wallNow, lastWall);
1570
+ if (!Number.isFinite(lastWall) || finite3(wallNow, lastWall) - lastWall >= minimumInterval - 0.01) {
1571
+ lastWall = finite3(wallNow, lastWall);
1535
1572
  status = "playing";
1536
1573
  publish("frame", false);
1537
1574
  }
@@ -1562,7 +1599,7 @@ function install2(__dmScope) {
1562
1599
  }
1563
1600
  function setQuality(value, reason) {
1564
1601
  var named = typeof value === "string" ? tiers.indexOf(value) : -1;
1565
- qualityIndex = named >= 0 ? named : clamp(Math.round(finite2(value, qualityIndex)), 0, tiers.length - 1);
1602
+ qualityIndex = named >= 0 ? named : clamp(Math.round(finite3(value, qualityIndex)), 0, tiers.length - 1);
1566
1603
  lastWall = -Infinity;
1567
1604
  publish(reason || "quality-change", audio.ended || blocked());
1568
1605
  }
@@ -1616,7 +1653,7 @@ function install2(__dmScope) {
1616
1653
  },
1617
1654
  setQuality,
1618
1655
  snapshot: function() {
1619
- 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) };
1656
+ 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) };
1620
1657
  },
1621
1658
  teardown: function() {
1622
1659
  stop("teardown", true);
@@ -1683,8 +1720,8 @@ function install2(__dmScope) {
1683
1720
  popover.appendChild(viewport);
1684
1721
  __dmScope.dataset(popover, "danceMovesLyricStage", "ready");
1685
1722
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", "0");
1686
- var travelTicks = Math.max(1, Math.round(finite2(options.travelTicks, 32)));
1687
- var defaultCueTicks = Math.max(1, Math.round(finite2(options.cueDurationTicks, 32)));
1723
+ var travelTicks = Math.max(1, Math.round(finite3(options.travelTicks, 32)));
1724
+ var defaultCueTicks = Math.max(1, Math.round(finite3(options.cueDurationTicks, 32)));
1688
1725
  var latest = null, phase = "idle", progress = 0, frame = 0, wakeTimer = 0, cueTimer = 0, cueCleanup = null, cueCount = 0;
1689
1726
  var lyricEffects = [];
1690
1727
  var slots = Object.freeze({ previous, current, next, viewport, track });
@@ -1733,7 +1770,7 @@ function install2(__dmScope) {
1733
1770
  publish(reason || value);
1734
1771
  }
1735
1772
  function setProgress(value) {
1736
- progress = clamp(finite2(value, 0), 0, 1);
1773
+ progress = clamp(finite3(value, 0), 0, 1);
1737
1774
  __dmScope.setStyle(popover.style, "--dance-moves-lyric-progress", progress.toFixed(5));
1738
1775
  }
1739
1776
  function copyLyrics(detail) {
@@ -1750,7 +1787,7 @@ function install2(__dmScope) {
1750
1787
  var cleanup = typeof result === "function" ? result : typeof result.cleanup === "function" ? result.cleanup : null;
1751
1788
  if (!cleanup)
1752
1789
  return;
1753
- var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite2(result.durationTicks, 64))) : 64;
1790
+ var durationTicks = result && typeof result === "object" ? Math.max(1, Math.round(finite3(result.durationTicks, 64))) : 64;
1754
1791
  var record = { cleanup, timer: 0 };
1755
1792
  record.timer = window2.setTimeout(function() {
1756
1793
  lyricEffects = lyricEffects.filter(function(item) {
@@ -1776,7 +1813,7 @@ function install2(__dmScope) {
1776
1813
  setProgress(0);
1777
1814
  return setPhase("waiting", "no-next-line");
1778
1815
  }
1779
- var now = Math.max(0, finite2(audio.currentTime, 0));
1816
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1780
1817
  if (now < windowRange.start)
1781
1818
  return reconcile("frame-before-window");
1782
1819
  if (now >= windowRange.end) {
@@ -1806,7 +1843,7 @@ function install2(__dmScope) {
1806
1843
  setProgress(0);
1807
1844
  return setPhase("waiting", reason || "no-next-line");
1808
1845
  }
1809
- var now = Math.max(0, finite2(audio.currentTime, 0));
1846
+ var now = Math.max(0, finite3(audio.currentTime, 0));
1810
1847
  if (now >= windowRange.end) {
1811
1848
  setProgress(1);
1812
1849
  return setPhase("arrived", reason || "arrived");
@@ -1821,7 +1858,7 @@ function install2(__dmScope) {
1821
1858
  wakeTimer = window2.setTimeout(function() {
1822
1859
  wakeTimer = 0;
1823
1860
  tick();
1824
- }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite2(audio.playbackRate, 1))));
1861
+ }, Math.max(0, (windowRange.start - now) * 1e3 / Math.max(0.01, finite3(audio.playbackRate, 1))));
1825
1862
  }
1826
1863
  function onLyric(detail) {
1827
1864
  var previousDetail = latest;
@@ -1876,7 +1913,7 @@ function install2(__dmScope) {
1876
1913
  else if (result && typeof result === "object") {
1877
1914
  if (typeof result.cleanup === "function")
1878
1915
  cueCleanup = result.cleanup;
1879
- durationTicks = Math.max(1, Math.round(finite2(result.durationTicks, defaultCueTicks)));
1916
+ durationTicks = Math.max(1, Math.round(finite3(result.durationTicks, defaultCueTicks)));
1880
1917
  }
1881
1918
  cueTimer = window2.setTimeout(function() {
1882
1919
  clearCue("complete");
@@ -1948,16 +1985,16 @@ function install2(__dmScope) {
1948
1985
  var hazardSources = Array.isArray(options.hazardSources) ? options.hazardSources.filter(Boolean).map(String) : [];
1949
1986
  if (!shipSources.length || !hazardSources.length)
1950
1987
  throw new Error("DanceMovesEffects cooperativeArena requires ship and hazard sources");
1951
- var shipCount = clamp(Math.round(finite2(options.shipCount, 3)), 2, 12);
1952
- var hazardCount = clamp(Math.round(finite2(options.hazardCount, 8)), 3, 48);
1953
- var speed = clamp(finite2(options.speed, 1), 0.35, 2.4);
1988
+ var shipCount = clamp(Math.round(finite3(options.shipCount, 3)), 2, 12);
1989
+ var hazardCount = clamp(Math.round(finite3(options.hazardCount, 8)), 3, 48);
1990
+ var speed = clamp(finite3(options.speed, 1), 0.35, 2.4);
1954
1991
  var inlineTransform = options.inlineTransform === true;
1955
- var bpm = Math.max(1, finite2(options.bpm, motion.bpm || 120));
1956
- var shotIntervalTicks = clamp(Math.round(finite2(options.shotIntervalTicks, 32)), 4, 256);
1992
+ var bpm = Math.max(1, finite3(options.bpm, motion.bpm || 120));
1993
+ var shotIntervalTicks = clamp(Math.round(finite3(options.shotIntervalTicks, 32)), 4, 256);
1957
1994
  var shotIntervalSeconds = motion.durationMilliseconds ? motion.durationMilliseconds(shotIntervalTicks, bpm) / 1e3 : shotIntervalTicks * 60 / (bpm * 16);
1958
- var burstDurationSeconds = clamp(finite2(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1959
- var shipHitboxScale = clamp(finite2(options.shipHitboxScale, 1), 0.4, 1);
1960
- var hazardHitboxScale = clamp(finite2(options.hazardHitboxScale, 1), 0.4, 1);
1995
+ var burstDurationSeconds = clamp(finite3(options.burstDurationBeats, 1.5), 0.1, 8) * 60 / bpm;
1996
+ var shipHitboxScale = clamp(finite3(options.shipHitboxScale, 1), 0.4, 1);
1997
+ var hazardHitboxScale = clamp(finite3(options.hazardHitboxScale, 1), 0.4, 1);
1961
1998
  var removers = state.removers, ships = [], hazards = [], shots = [], bursts = [], burstTimers = [];
1962
1999
  var activeShips = shipCount, activeHazards = hazardCount;
1963
2000
  var width = 1, height = 1, frame = 0, last = 0, elapsed = 0, nextShot = 0, hits = 0;
@@ -2289,7 +2326,7 @@ function install2(__dmScope) {
2289
2326
  reconcile(reason || "manual");
2290
2327
  },
2291
2328
  setDensity: function(nextShips, nextHazards) {
2292
- var shipsNext = clamp(Math.round(finite2(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite2(nextHazards, activeHazards)), 3, hazardCount);
2329
+ var shipsNext = clamp(Math.round(finite3(nextShips, activeShips)), 2, shipCount), hazardsNext = clamp(Math.round(finite3(nextHazards, activeHazards)), 3, hazardCount);
2293
2330
  if (shipsNext === activeShips && hazardsNext === activeHazards)
2294
2331
  return;
2295
2332
  activeShips = shipsNext;
@@ -2332,12 +2369,12 @@ function install2(__dmScope) {
2332
2369
  options = options || {};
2333
2370
  var state = commonState(options, "quality");
2334
2371
  var tiers = Array.isArray(options.tiers) && options.tiers.length ? options.tiers.map(String) : ["full", "constrained", "minimal"];
2335
- var index = clamp(Math.round(finite2(options.initial, 0)), 0, tiers.length - 1);
2336
- var sampleMs = Math.max(800, finite2(options.sampleMilliseconds, 1600));
2337
- var poorWindows = Math.max(2, Math.round(finite2(options.poorWindows, 2)));
2338
- var healthyWindows = Math.max(3, Math.round(finite2(options.healthyWindows, 5)));
2339
- var downgradeRatio = clamp(finite2(options.downgradeRatio, 0.72), 0.2, 0.95);
2340
- var recoveryRatio = clamp(finite2(options.recoveryRatio, 0.9), downgradeRatio, 1);
2372
+ var index = clamp(Math.round(finite3(options.initial, 0)), 0, tiers.length - 1);
2373
+ var sampleMs = Math.max(800, finite3(options.sampleMilliseconds, 1600));
2374
+ var poorWindows = Math.max(2, Math.round(finite3(options.poorWindows, 2)));
2375
+ var healthyWindows = Math.max(3, Math.round(finite3(options.healthyWindows, 5)));
2376
+ var downgradeRatio = clamp(finite3(options.downgradeRatio, 0.72), 0.2, 0.95);
2377
+ var recoveryRatio = clamp(finite3(options.recoveryRatio, 0.9), downgradeRatio, 1);
2341
2378
  var frame = 0, frames = 0, start = 0, reference = 0, poor = 0, healthy = 0, lastShift = 0, running = true;
2342
2379
  var preferenceLimited = false, tierBeforePreference = index;
2343
2380
  function publish(reason, sample) {
@@ -2381,12 +2418,12 @@ function install2(__dmScope) {
2381
2418
  poor = 0;
2382
2419
  healthy = 0;
2383
2420
  }
2384
- if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite2(options.downgradeCooldownMilliseconds, 5e3)) {
2421
+ if (poor >= poorWindows && index < tiers.length - 1 && now - lastShift >= finite3(options.downgradeCooldownMilliseconds, 5e3)) {
2385
2422
  index += 1;
2386
2423
  poor = 0;
2387
2424
  lastShift = now;
2388
2425
  publish("sustained-low-fps", { fps });
2389
- } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite2(options.recoveryCooldownMilliseconds, 12e3)) {
2426
+ } else if (healthy >= healthyWindows && index > 0 && now - lastShift >= finite3(options.recoveryCooldownMilliseconds, 12e3)) {
2390
2427
  index -= 1;
2391
2428
  healthy = 0;
2392
2429
  lastShift = now;
@@ -2425,7 +2462,7 @@ function install2(__dmScope) {
2425
2462
  id: state.id,
2426
2463
  type: "quality",
2427
2464
  setTier: function(value, reason) {
2428
- index = clamp(Math.round(finite2(value, index)), 0, tiers.length - 1);
2465
+ index = clamp(Math.round(finite3(value, index)), 0, tiers.length - 1);
2429
2466
  publish(reason || "manual");
2430
2467
  },
2431
2468
  snapshot: function() {
@@ -4895,8 +4932,9 @@ function install9(__dmScope) {
4895
4932
  anchor = 0;
4896
4933
  lastClock = mode;
4897
4934
  const tempo = bpm();
4898
- const beats = useAudio ? Math.max(0, Number.isFinite(audio.currentTime) ? audio.currentTime : 0) * tempo / 60 : motion.currentTick({ clock: "page" }) / 16;
4899
- return { beats, mode, tempo };
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.beats,
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 bpm = explicit ? Math.round(finite(input.bpm, "bpm", 20, 400) * 1e3) / 1e3 : 120;
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: explicit && input.bpmSource !== "fallback" ? "explicit" : "fallback",
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 numeric = (value) => value !== "" && value !== null && value !== void 0 && Number.isFinite(Number(value));
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 = numeric(b) && Number(b) >= 20 && Number(b) <= 400 ? Number(b) : void 0;
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);
@@ -6403,6 +6486,184 @@ function installPlayback(scope, api, host = scope.document.defaultView) {
6403
6486
  });
6404
6487
  }
6405
6488
 
6489
+ // .build/modules/recordings.mjs
6490
+ var WASM = "AGFzbQEAAAABDwJgAAF/YAZ/f39/f38BfAMDAgABBQUBAQKAQAYIAX8BQYCIBAsHKgMGbWVtb3J5AgAKdGFibGVfYmFzZQAAEHNhbXBsZV9yZWNvcmRpbmcAAQpvAggAQYCIhIAAC2QCAXwBfkQAAAAAAAD4fyEGAkAgAEUNACABQQFIDQAgAkEBSA0AIAVBAEgNACAFIAJPDQAgBKwgA6x9IgdCAFMNACAHIAGtWg0AIAAgB6cgAmxBA3RqIAVBA3RqKwMAIQYLIAYL";
6491
+ var MAX_VALUES = 8388608;
6492
+ var MAX_BYTES = 64 * 1024 * 1024;
6493
+ var modulePromise;
6494
+ var finite2 = (n, label) => {
6495
+ if (typeof n !== "number" || !Number.isFinite(n)) throw new TypeError(label + " must be finite");
6496
+ return n;
6497
+ };
6498
+ function validateRecording(input) {
6499
+ 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");
6500
+ if (typeof input.name !== "string" || !input.name.length || input.name.length > 128) throw new TypeError("Recording name is required");
6501
+ const dimensions = input.dimensions;
6502
+ 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");
6503
+ const c = input.clock;
6504
+ 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");
6505
+ finite2(c.beat_zero_seconds, "Beat zero");
6506
+ if (!Number.isInteger(c.start_pip) || c.start_pip < -2147483648) throw new RangeError("Invalid recording start pip");
6507
+ const rows = input.samples;
6508
+ 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");
6509
+ const values = new Float64Array(rows.length * dimensions.length);
6510
+ rows.forEach((row, i) => {
6511
+ if (!Array.isArray(row) || row.length !== dimensions.length) throw new TypeError("Recording sample dimensions differ");
6512
+ row.forEach((v, j) => {
6513
+ finite2(v, "Sample");
6514
+ if (Math.abs(v) > 1) throw new RangeError("Recording samples must be in [-1,1]");
6515
+ values[i * dimensions.length + j] = v;
6516
+ });
6517
+ });
6518
+ return Object.freeze({ name: input.name, dimensions: Object.freeze([...dimensions]), count: rows.length, clock: Object.freeze({ ...c }), values });
6519
+ }
6520
+ async function samplerFromData(data) {
6521
+ if (!modulePromise) modulePromise = WebAssembly.compile(Uint8Array.from(atob(WASM), (c) => c.charCodeAt(0))).catch((error) => {
6522
+ modulePromise = null;
6523
+ throw error;
6524
+ });
6525
+ const api = (await WebAssembly.instantiate(await modulePromise, {})).exports;
6526
+ const base = api.table_base(), pages = Math.ceil((base + data.values.byteLength - api.memory.buffer.byteLength) / 65536);
6527
+ if (pages > 0) api.memory.grow(pages);
6528
+ new Float64Array(api.memory.buffer, base, data.values.length).set(data.values);
6529
+ return Object.freeze({ ...data, sample(pip) {
6530
+ 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");
6531
+ return data.dimensions.map((_, axis) => api.sample_recording(base, data.count, data.dimensions.length, data.clock.start_pip, pip, axis));
6532
+ } });
6533
+ }
6534
+ function createRecordingService(scope, rudiments, config) {
6535
+ const handles = /* @__PURE__ */ new Map();
6536
+ async function load(url2) {
6537
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6538
+ const response = await scope.window.fetch(url2);
6539
+ if (!response.ok) throw new Error("Recording request failed: " + response.status);
6540
+ if (Number(response.headers?.get("content-length")) > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6541
+ let text;
6542
+ if (response.body?.getReader) {
6543
+ const reader = response.body.getReader(), chunks = [];
6544
+ let size = 0;
6545
+ for (; ; ) {
6546
+ const { done, value } = await reader.read();
6547
+ if (done) break;
6548
+ size += value.byteLength;
6549
+ if (size > MAX_BYTES) {
6550
+ await reader.cancel();
6551
+ throw new RangeError("Recording exceeds 64 MiB");
6552
+ }
6553
+ chunks.push(value);
6554
+ }
6555
+ const bytes = new Uint8Array(size);
6556
+ let offset = 0;
6557
+ for (const chunk of chunks) {
6558
+ bytes.set(chunk, offset);
6559
+ offset += chunk.length;
6560
+ }
6561
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
6562
+ } else {
6563
+ text = await response.text();
6564
+ if (new TextEncoder().encode(text).length > MAX_BYTES) throw new RangeError("Recording exceeds 64 MiB");
6565
+ }
6566
+ const input = JSON.parse(text);
6567
+ validateRecording(input);
6568
+ return input;
6569
+ }
6570
+ function animate(options = {}) {
6571
+ if (scope.disposed) throw new Error("DanceMoves mount is destroyed");
6572
+ if (typeof options.id !== "string" || !options.id.trim() || handles.has(options.id)) throw new TypeError("Recording controller requires a unique id");
6573
+ if (typeof options.render !== "function") throw new TypeError("Recording render callback is required");
6574
+ if (!options.audio) throw new TypeError("Recording playback requires an audio/player source");
6575
+ if (["rate", "phasePips", "clock", "bpm"].some((k) => Object.hasOwn(options, k))) throw new TypeError("Recording clock, rate and origin come from its capture");
6576
+ const data = validateRecording(options.recording);
6577
+ if (Math.abs(data.clock.bpm - config.bpm) > 1e-9) throw new RangeError("Recording BPM does not match the authoritative page BPM");
6578
+ let inner = null, sampler = null, destroyed = false, enabled = options.enabled !== false, paused = false, status = "loading", error = null, last = null;
6579
+ const output = (frame, values, pip, reason) => {
6580
+ if (destroyed) return;
6581
+ 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 };
6582
+ options.render(last);
6583
+ };
6584
+ const handle = {
6585
+ id: options.id,
6586
+ ready: null,
6587
+ setEnabled(value) {
6588
+ if (typeof value !== "boolean") throw new TypeError("enabled must be boolean");
6589
+ enabled = value;
6590
+ inner?.setEnabled(value);
6591
+ },
6592
+ pause() {
6593
+ paused = true;
6594
+ inner?.pause();
6595
+ },
6596
+ resume() {
6597
+ paused = false;
6598
+ inner?.resume();
6599
+ },
6600
+ refresh() {
6601
+ inner?.refresh();
6602
+ },
6603
+ destroy() {
6604
+ if (destroyed) return;
6605
+ inner?.destroy();
6606
+ inner = null;
6607
+ sampler = null;
6608
+ destroyed = true;
6609
+ status = "destroyed";
6610
+ handles.delete(options.id);
6611
+ },
6612
+ snapshot() {
6613
+ const state = inner?.snapshot() ?? null;
6614
+ 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 };
6615
+ }
6616
+ };
6617
+ handle.teardown = handle.destroy;
6618
+ handles.set(options.id, handle);
6619
+ handle.ready = samplerFromData(data).then(async (native) => {
6620
+ if (destroyed || scope.disposed) return handle;
6621
+ sampler = native;
6622
+ inner = rudiments.animate({
6623
+ id: options.id,
6624
+ root: options.root,
6625
+ target: options.target,
6626
+ audio: options.audio,
6627
+ clock: "audio",
6628
+ rudiment: "circle",
6629
+ rate: 1,
6630
+ phasePips: 0,
6631
+ amplitude: 0,
6632
+ css: false,
6633
+ renderEveryFrame: true,
6634
+ offscreen: options.offscreen !== false,
6635
+ enabled,
6636
+ render(frame) {
6637
+ if (destroyed) return;
6638
+ const neutral = ["disabled", "reduced-motion", "forced-colors"].includes(frame.reason), precise = (frame.sourceBeats - data.clock.beat_zero_seconds * data.clock.bpm / 60) * 64;
6639
+ const pip = Math.floor(precise + 1e-8), outside = pip < data.clock.start_pip || pip >= data.clock.start_pip + data.count;
6640
+ status = neutral ? frame.reason : outside ? "outside-recording" : frame.reason;
6641
+ const values = neutral || outside ? data.dimensions.map(() => 0) : sampler.sample(pip);
6642
+ output(frame, values, pip, status);
6643
+ }
6644
+ });
6645
+ if (paused) inner.pause();
6646
+ await inner.ready;
6647
+ return handle;
6648
+ }).catch((failure) => {
6649
+ error = String(failure.message || failure);
6650
+ status = "error";
6651
+ inner?.destroy();
6652
+ inner = null;
6653
+ sampler = null;
6654
+ if (handles.get(options.id) === handle) handles.delete(options.id);
6655
+ throw failure;
6656
+ });
6657
+ return handle;
6658
+ }
6659
+ scope.onDispose(() => {
6660
+ for (const handle of [...handles.values()]) handle.destroy();
6661
+ });
6662
+ return Object.freeze({ load, animate, get: (id) => handles.get(id) || null, destroyAll: () => {
6663
+ for (const h of [...handles.values()]) h.destroy();
6664
+ }, snapshot: () => ({ native: "DanceRudiments 0.3.0 recording C++/WASM", instances: [...handles.values()].map((h) => h.snapshot()) }) });
6665
+ }
6666
+
6406
6667
  // .build/modules/child-playback.mjs
6407
6668
  var PROTOCOL = "dancemoves.child-playback.v1";
6408
6669
  var EVENTS = /* @__PURE__ */ new Set(["snapshot", "loadedmetadata", "durationchange", "play", "playing", "pause", "ended", "seeking", "seeked", "ratechange", "timeupdate", "waiting", "error"]);
@@ -6827,7 +7088,7 @@ function installChildPlayback(scope, api, host) {
6827
7088
  }
6828
7089
 
6829
7090
  // .build/modules/version.mjs
6830
- var VERSION = "3.1.23";
7091
+ var VERSION = "3.3.0";
6831
7092
 
6832
7093
  // .build/modules/index.mjs
6833
7094
  var mounted = /* @__PURE__ */ new WeakMap();
@@ -6880,6 +7141,7 @@ function createDanceMoves(options = {}) {
6880
7141
  config,
6881
7142
  effects: w.DanceMovesEffects,
6882
7143
  rudiments: w.DanceMovesRudiments,
7144
+ recordings: createRecordingService(scope, w.DanceMovesRudiments, config),
6883
7145
  orientationCore: w.KSEpkOrientationCore,
6884
7146
  getOrientation: () => w.__ksEpkOrientationRuntime || null,
6885
7147
  getClay: () => w.DanceMovesClayStars || null,
@@ -6921,5 +7183,6 @@ export {
6921
7183
  createPlaybackPublisher,
6922
7184
  createPlaybackReceiver,
6923
7185
  fromWordPressMeta,
7186
+ validateBeatAlignment,
6924
7187
  validatePageConfig
6925
7188
  };