@voicethere/agent 0.7.12 → 0.7.13
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.
|
@@ -1137,6 +1137,8 @@ function resolveClipUrl(assetOrigin, clipId) {
|
|
|
1137
1137
|
var SHOWCASE_DEMOS = /* @__PURE__ */ new Set(["orbit", "soundboard", "proximity"]);
|
|
1138
1138
|
var MAX_SAY_TEXT_LENGTH = 200;
|
|
1139
1139
|
var MAX_XZ = 5;
|
|
1140
|
+
var ORBIT_SINE_MIN_FREQUENCY_HZ = 110;
|
|
1141
|
+
var ORBIT_SINE_MAX_FREQUENCY_HZ = 1760;
|
|
1140
1142
|
function isFiniteNumber(value) {
|
|
1141
1143
|
return typeof value === "number" && Number.isFinite(value);
|
|
1142
1144
|
}
|
|
@@ -1146,6 +1148,9 @@ function clampVolume(value) {
|
|
|
1146
1148
|
function isInXzRange(value) {
|
|
1147
1149
|
return value >= -MAX_XZ && value <= MAX_XZ;
|
|
1148
1150
|
}
|
|
1151
|
+
function isOrbitSineFrequencyHz(value) {
|
|
1152
|
+
return value >= ORBIT_SINE_MIN_FREQUENCY_HZ && value <= ORBIT_SINE_MAX_FREQUENCY_HZ;
|
|
1153
|
+
}
|
|
1149
1154
|
function isMixPlacement(value) {
|
|
1150
1155
|
return value === "center" || value === "left" || value === "right" || value === "front" || value === "behind" || value === "below" || value === "above";
|
|
1151
1156
|
}
|
|
@@ -1194,15 +1199,33 @@ function parseShowcaseMessage(message) {
|
|
|
1194
1199
|
if (set.paused !== void 0 && typeof set.paused !== "boolean") {
|
|
1195
1200
|
return null;
|
|
1196
1201
|
}
|
|
1202
|
+
if (set.frequencyHz !== void 0 && (!isFiniteNumber(set.frequencyHz) || !isOrbitSineFrequencyHz(set.frequencyHz))) {
|
|
1203
|
+
return null;
|
|
1204
|
+
}
|
|
1205
|
+
if (set.sinePlaying !== void 0 && typeof set.sinePlaying !== "boolean") {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1197
1208
|
return {
|
|
1198
1209
|
type: "orbit",
|
|
1199
1210
|
action: "set",
|
|
1200
1211
|
...set.radius !== void 0 ? { radius: set.radius } : {},
|
|
1201
1212
|
...set.periodSec !== void 0 ? { periodSec: set.periodSec } : {},
|
|
1202
1213
|
...set.elevation !== void 0 ? { elevation: set.elevation } : {},
|
|
1203
|
-
...set.paused !== void 0 ? { paused: set.paused } : {}
|
|
1214
|
+
...set.paused !== void 0 ? { paused: set.paused } : {},
|
|
1215
|
+
...set.frequencyHz !== void 0 ? { frequencyHz: set.frequencyHz } : {},
|
|
1216
|
+
...set.sinePlaying !== void 0 ? { sinePlaying: set.sinePlaying } : {}
|
|
1204
1217
|
};
|
|
1205
1218
|
}
|
|
1219
|
+
if (orbit.action === "place") {
|
|
1220
|
+
const place = message;
|
|
1221
|
+
if (!isFiniteNumber(place.x) || !isInXzRange(place.x)) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
if (!isFiniteNumber(place.z) || !isInXzRange(place.z)) {
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
return { type: "orbit", action: "place", x: place.x, z: place.z };
|
|
1228
|
+
}
|
|
1206
1229
|
if (orbit.action === "say") {
|
|
1207
1230
|
const say = message;
|
|
1208
1231
|
if (typeof say.text !== "string" || say.text.length === 0 || say.text.length > MAX_SAY_TEXT_LENGTH) {
|
|
@@ -1254,6 +1277,24 @@ function parseShowcaseMessage(message) {
|
|
|
1254
1277
|
}
|
|
1255
1278
|
return { type: "pad_status", playId: status.playId.trim() };
|
|
1256
1279
|
}
|
|
1280
|
+
case "pad_move": {
|
|
1281
|
+
const padMove = message;
|
|
1282
|
+
if (!isNonEmptyString(padMove.playId)) {
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
if (!isFiniteNumber(padMove.x) || !isInXzRange(padMove.x)) {
|
|
1286
|
+
return null;
|
|
1287
|
+
}
|
|
1288
|
+
if (!isFiniteNumber(padMove.z) || !isInXzRange(padMove.z)) {
|
|
1289
|
+
return null;
|
|
1290
|
+
}
|
|
1291
|
+
return {
|
|
1292
|
+
type: "pad_move",
|
|
1293
|
+
playId: padMove.playId.trim(),
|
|
1294
|
+
x: padMove.x,
|
|
1295
|
+
z: padMove.z
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1257
1298
|
case "move": {
|
|
1258
1299
|
const move = message;
|
|
1259
1300
|
if (!isFiniteNumber(move.x) || !isInXzRange(move.x)) {
|
|
@@ -1368,12 +1409,22 @@ var ProximityRoom = class {
|
|
|
1368
1409
|
var ORBIT_SINE_DURATION_MS = 2e3;
|
|
1369
1410
|
var ORBIT_SINE_FREQUENCY_HZ = 440;
|
|
1370
1411
|
var ORBIT_SINE_AMPLITUDE = 8e3;
|
|
1412
|
+
var ORBIT_SINE_SAMPLE_RATE = 16e3;
|
|
1371
1413
|
var ORBIT_SINE_INLINE_URL = "https://example.com/inline-orbit-sine.wav";
|
|
1372
|
-
function
|
|
1373
|
-
const
|
|
1414
|
+
function orbitSineSampleCount(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, sampleRate = ORBIT_SINE_SAMPLE_RATE) {
|
|
1415
|
+
const targetCycles = durationMs / 1e3 * frequencyHz;
|
|
1416
|
+
const cycles = Math.max(1, Math.floor(targetCycles));
|
|
1417
|
+
return Math.round(cycles * sampleRate / frequencyHz);
|
|
1418
|
+
}
|
|
1419
|
+
function orbitSineClipDurationMs(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, sampleRate = ORBIT_SINE_SAMPLE_RATE) {
|
|
1420
|
+
const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
|
|
1421
|
+
return numSamples / sampleRate * 1e3;
|
|
1422
|
+
}
|
|
1423
|
+
function buildOrbitSineInlineClipBase64(durationMs = ORBIT_SINE_DURATION_MS, frequencyHz = ORBIT_SINE_FREQUENCY_HZ, amplitude = ORBIT_SINE_AMPLITUDE, phaseRad = 0) {
|
|
1424
|
+
const sampleRate = ORBIT_SINE_SAMPLE_RATE;
|
|
1374
1425
|
const channels = 1;
|
|
1375
1426
|
const bytesPerSample = 2;
|
|
1376
|
-
const numSamples =
|
|
1427
|
+
const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
|
|
1377
1428
|
const dataSize = numSamples * channels * bytesPerSample;
|
|
1378
1429
|
const buffer = Buffer.alloc(44 + dataSize);
|
|
1379
1430
|
buffer.write("RIFF", 0);
|
|
@@ -1392,7 +1443,7 @@ function buildOrbitSineInlineClipBase64(durationMs = ORBIT_SINE_DURATION_MS, fre
|
|
|
1392
1443
|
for (let i = 0; i < numSamples; i++) {
|
|
1393
1444
|
const t = i / sampleRate;
|
|
1394
1445
|
const sample = Math.round(
|
|
1395
|
-
amplitude * Math.sin(2 * Math.PI * frequencyHz * t)
|
|
1446
|
+
amplitude * Math.sin(2 * Math.PI * frequencyHz * t + phaseRad)
|
|
1396
1447
|
);
|
|
1397
1448
|
buffer.writeInt16LE(sample, 44 + i * 2);
|
|
1398
1449
|
}
|
|
@@ -1405,6 +1456,7 @@ var ORBIT_POSE_EMIT_MS = 100;
|
|
|
1405
1456
|
var ROOM_STATE_TICK_MS = 100;
|
|
1406
1457
|
var LOOP_PAD_POLL_MS = 250;
|
|
1407
1458
|
var ORBIT_SINE_POLL_MS = 250;
|
|
1459
|
+
var ORBIT_SINE_GAPLESS_LEAD_MS = 200;
|
|
1408
1460
|
var ORBIT_SINE_VOLUME = 0.25;
|
|
1409
1461
|
var MAX_ACTIVE_PLAYS = 12;
|
|
1410
1462
|
var SHOWCASE_ROOM_GROUP_ID = "showcase-room";
|
|
@@ -1467,6 +1519,9 @@ function defaultSessionState(demo, assetOrigin) {
|
|
|
1467
1519
|
playIds: /* @__PURE__ */ new Set(),
|
|
1468
1520
|
loopPads: /* @__PURE__ */ new Map(),
|
|
1469
1521
|
loopPollTimers: /* @__PURE__ */ new Map(),
|
|
1522
|
+
orbitSineFrequencyHz: 440,
|
|
1523
|
+
orbitSineEnabled: true,
|
|
1524
|
+
orbitSinePhaseRad: 0,
|
|
1470
1525
|
inProximityRoom: false
|
|
1471
1526
|
};
|
|
1472
1527
|
}
|
|
@@ -1508,27 +1563,114 @@ function clearAllPlays(sessionId, state) {
|
|
|
1508
1563
|
state.loopPollTimers.clear();
|
|
1509
1564
|
}
|
|
1510
1565
|
function currentOrbitPose(state) {
|
|
1566
|
+
if (state.orbitManualPose) {
|
|
1567
|
+
const elevation = state.orbit.elevation ?? 0;
|
|
1568
|
+
return {
|
|
1569
|
+
position: {
|
|
1570
|
+
x: state.orbitManualPose.x,
|
|
1571
|
+
y: elevation,
|
|
1572
|
+
z: state.orbitManualPose.z
|
|
1573
|
+
},
|
|
1574
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 }
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1511
1577
|
if (!state.orbitStartMs) {
|
|
1512
1578
|
return orbitTtsPose(0, state.orbit);
|
|
1513
1579
|
}
|
|
1514
1580
|
const elapsedSec = (Date.now() - state.orbitStartMs) / 1e3;
|
|
1515
1581
|
return orbitTtsPose(elapsedSec, state.orbit);
|
|
1516
1582
|
}
|
|
1583
|
+
function orbitPoseFromXz(state, x, z) {
|
|
1584
|
+
const elevation = state.orbit.elevation ?? 0;
|
|
1585
|
+
return {
|
|
1586
|
+
position: { x, y: elevation, z },
|
|
1587
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 }
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
function syncOrbitClockFromManualPose(state) {
|
|
1591
|
+
const manual = state.orbitManualPose;
|
|
1592
|
+
if (!manual) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const periodSec = state.orbit.periodSec ?? 2 * Math.PI;
|
|
1596
|
+
const angleRad = Math.atan2(manual.z, manual.x);
|
|
1597
|
+
const elapsedSec = angleRad * periodSec / (2 * Math.PI);
|
|
1598
|
+
state.orbitStartMs = Date.now() - elapsedSec * 1e3;
|
|
1599
|
+
state.orbitManualPose = void 0;
|
|
1600
|
+
}
|
|
1517
1601
|
function clearOrbitSinePoll(state) {
|
|
1518
1602
|
if (state.orbitSinePollTimer) {
|
|
1519
1603
|
clearInterval(state.orbitSinePollTimer);
|
|
1520
1604
|
state.orbitSinePollTimer = void 0;
|
|
1521
1605
|
}
|
|
1522
1606
|
}
|
|
1607
|
+
function clearOrbitSineRestartTimer(state) {
|
|
1608
|
+
if (state.orbitSineRestartTimer) {
|
|
1609
|
+
clearTimeout(state.orbitSineRestartTimer);
|
|
1610
|
+
state.orbitSineRestartTimer = void 0;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1523
1613
|
async function stopOrbitSine(state) {
|
|
1524
1614
|
clearOrbitSinePoll(state);
|
|
1615
|
+
clearOrbitSineRestartTimer(state);
|
|
1525
1616
|
const playId = state.orbitSinePlayId;
|
|
1526
1617
|
state.orbitSinePlayId = void 0;
|
|
1527
1618
|
if (playId) {
|
|
1528
1619
|
await stopPlay(playId);
|
|
1529
1620
|
}
|
|
1530
1621
|
}
|
|
1622
|
+
function orbitSineClipBytes(state) {
|
|
1623
|
+
return buildOrbitSineInlineClipBase64(
|
|
1624
|
+
ORBIT_SINE_DURATION_MS,
|
|
1625
|
+
state.orbitSineFrequencyHz,
|
|
1626
|
+
void 0,
|
|
1627
|
+
state.orbitSinePhaseRad
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
function orbitSineClipDuration(state) {
|
|
1631
|
+
return orbitSineClipDurationMs(
|
|
1632
|
+
ORBIT_SINE_DURATION_MS,
|
|
1633
|
+
state.orbitSineFrequencyHz
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
function scheduleOrbitSineGaplessRestart(sessionId, state) {
|
|
1637
|
+
clearOrbitSineRestartTimer(state);
|
|
1638
|
+
const durationMs = orbitSineClipDuration(state);
|
|
1639
|
+
const delayMs = Math.max(0, durationMs - ORBIT_SINE_GAPLESS_LEAD_MS);
|
|
1640
|
+
state.orbitSineRestartTimer = setTimeout(() => {
|
|
1641
|
+
state.orbitSineRestartTimer = void 0;
|
|
1642
|
+
void restartOrbitSineGapless(sessionId, state);
|
|
1643
|
+
}, delayMs);
|
|
1644
|
+
}
|
|
1645
|
+
async function restartOrbitSineGapless(sessionId, state) {
|
|
1646
|
+
if (!state.orbitSineEnabled || !state.orbitSinePlayId) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const previousPlayId = state.orbitSinePlayId;
|
|
1650
|
+
const result = await play({
|
|
1651
|
+
url: ORBIT_SINE_INLINE_URL,
|
|
1652
|
+
bytes: orbitSineClipBytes(state),
|
|
1653
|
+
sessionIds: [sessionId],
|
|
1654
|
+
volume: ORBIT_SINE_VOLUME,
|
|
1655
|
+
pose: currentOrbitPose(state)
|
|
1656
|
+
});
|
|
1657
|
+
if (!result.ok || !result.playId) {
|
|
1658
|
+
agentLog(
|
|
1659
|
+
"warn",
|
|
1660
|
+
`spatial-showcase orbit sine gapless restart failed: ${result.reason ?? "play_failed"}`,
|
|
1661
|
+
sessionId
|
|
1662
|
+
);
|
|
1663
|
+
startOrbitSineLoopPoll(sessionId, state);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
state.orbitSinePlayId = result.playId;
|
|
1667
|
+
void stopPlay(previousPlayId);
|
|
1668
|
+
scheduleOrbitSineGaplessRestart(sessionId, state);
|
|
1669
|
+
}
|
|
1531
1670
|
function startOrbitSineLoopPoll(sessionId, state) {
|
|
1671
|
+
if (state.orbitSineRestartTimer) {
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1532
1674
|
clearOrbitSinePoll(state);
|
|
1533
1675
|
const playId = state.orbitSinePlayId;
|
|
1534
1676
|
if (!playId) {
|
|
@@ -1536,7 +1678,7 @@ function startOrbitSineLoopPoll(sessionId, state) {
|
|
|
1536
1678
|
}
|
|
1537
1679
|
state.orbitSinePollTimer = setInterval(() => {
|
|
1538
1680
|
void (async () => {
|
|
1539
|
-
if (!state.orbitSinePlayId) {
|
|
1681
|
+
if (!state.orbitSinePlayId || state.orbitSineRestartTimer) {
|
|
1540
1682
|
clearOrbitSinePoll(state);
|
|
1541
1683
|
return;
|
|
1542
1684
|
}
|
|
@@ -1545,25 +1687,19 @@ function startOrbitSineLoopPoll(sessionId, state) {
|
|
|
1545
1687
|
return;
|
|
1546
1688
|
}
|
|
1547
1689
|
if (status.status === "ended") {
|
|
1548
|
-
|
|
1549
|
-
url: ORBIT_SINE_INLINE_URL,
|
|
1550
|
-
bytes: buildOrbitSineInlineClipBase64(),
|
|
1551
|
-
sessionIds: [sessionId],
|
|
1552
|
-
volume: ORBIT_SINE_VOLUME,
|
|
1553
|
-
pose: currentOrbitPose(state)
|
|
1554
|
-
});
|
|
1555
|
-
if (replay.ok && replay.playId) {
|
|
1556
|
-
state.orbitSinePlayId = replay.playId;
|
|
1557
|
-
}
|
|
1690
|
+
await restartOrbitSineGapless(sessionId, state);
|
|
1558
1691
|
}
|
|
1559
1692
|
})();
|
|
1560
1693
|
}, ORBIT_SINE_POLL_MS);
|
|
1561
1694
|
}
|
|
1562
1695
|
async function startOrbitSinePlay(sessionId, state) {
|
|
1696
|
+
if (!state.orbitSineEnabled) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1563
1699
|
await stopOrbitSine(state);
|
|
1564
1700
|
const result = await play({
|
|
1565
1701
|
url: ORBIT_SINE_INLINE_URL,
|
|
1566
|
-
bytes:
|
|
1702
|
+
bytes: orbitSineClipBytes(state),
|
|
1567
1703
|
sessionIds: [sessionId],
|
|
1568
1704
|
volume: ORBIT_SINE_VOLUME,
|
|
1569
1705
|
pose: currentOrbitPose(state)
|
|
@@ -1577,10 +1713,11 @@ async function startOrbitSinePlay(sessionId, state) {
|
|
|
1577
1713
|
return;
|
|
1578
1714
|
}
|
|
1579
1715
|
state.orbitSinePlayId = result.playId;
|
|
1716
|
+
scheduleOrbitSineGaplessRestart(sessionId, state);
|
|
1580
1717
|
startOrbitSineLoopPoll(sessionId, state);
|
|
1581
1718
|
}
|
|
1582
1719
|
function emitOrbitPose(sessionId, state) {
|
|
1583
|
-
if (!state.orbitStartMs) {
|
|
1720
|
+
if (!state.orbitStartMs && !state.orbitManualPose) {
|
|
1584
1721
|
return;
|
|
1585
1722
|
}
|
|
1586
1723
|
const now = Date.now();
|
|
@@ -1588,9 +1725,8 @@ function emitOrbitPose(sessionId, state) {
|
|
|
1588
1725
|
return;
|
|
1589
1726
|
}
|
|
1590
1727
|
state.lastOrbitPoseEmitMs = now;
|
|
1591
|
-
const
|
|
1592
|
-
const
|
|
1593
|
-
const angleRad = 2 * Math.PI * elapsedSec / (state.orbit.periodSec ?? 2 * Math.PI);
|
|
1728
|
+
const pose = currentOrbitPose(state);
|
|
1729
|
+
const angleRad = state.orbitManualPose ? Math.atan2(state.orbitManualPose.z, state.orbitManualPose.x) : 2 * Math.PI * (now - state.orbitStartMs) / 1e3 / (state.orbit.periodSec ?? 2 * Math.PI);
|
|
1594
1730
|
sendToClient(sessionId, {
|
|
1595
1731
|
type: "orbit_pose",
|
|
1596
1732
|
angleDeg: angleRad * 180 / Math.PI,
|
|
@@ -1598,12 +1734,22 @@ function emitOrbitPose(sessionId, state) {
|
|
|
1598
1734
|
z: pose.position.z
|
|
1599
1735
|
});
|
|
1600
1736
|
}
|
|
1737
|
+
function emitOrbitPlacePose(sessionId, state, x, z) {
|
|
1738
|
+
state.lastOrbitPoseEmitMs = Date.now();
|
|
1739
|
+
const angleRad = Math.atan2(z, x);
|
|
1740
|
+
sendToClient(sessionId, {
|
|
1741
|
+
type: "orbit_pose",
|
|
1742
|
+
angleDeg: angleRad * 180 / Math.PI,
|
|
1743
|
+
x,
|
|
1744
|
+
z
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1601
1747
|
function startOrbitDemo(sessionId, state) {
|
|
1602
1748
|
stopOrbitInterval(state);
|
|
1603
1749
|
beginOrbitClock(state);
|
|
1604
1750
|
void startOrbitSinePlay(sessionId, state);
|
|
1605
1751
|
state.orbitTimer = setInterval(() => {
|
|
1606
|
-
if (state.orbit.paused || !state.orbitStartMs) {
|
|
1752
|
+
if (state.orbit.paused || !state.orbitStartMs && !state.orbitManualPose) {
|
|
1607
1753
|
return;
|
|
1608
1754
|
}
|
|
1609
1755
|
const pose = currentOrbitPose(state);
|
|
@@ -1758,6 +1904,24 @@ async function handlePadPlay(sessionId, state, clipId, x, z, volume, placement)
|
|
|
1758
1904
|
void startLoopPadPoll(sessionId, state, result.playId, pad);
|
|
1759
1905
|
}
|
|
1760
1906
|
}
|
|
1907
|
+
async function handlePadMove(sessionId, state, playId, x, z) {
|
|
1908
|
+
if (!state.playIds.has(playId)) {
|
|
1909
|
+
ackError(sessionId, "pad_move", "unknown_play");
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
const poseResult = await setPlayPose(playId, poseAt(x, z));
|
|
1913
|
+
if (!poseResult.ok) {
|
|
1914
|
+
ackError(sessionId, "pad_move", poseResult.reason ?? "set_pose_failed");
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
const loopPad = state.loopPads.get(playId);
|
|
1918
|
+
if (loopPad) {
|
|
1919
|
+
loopPad.x = x;
|
|
1920
|
+
loopPad.z = z;
|
|
1921
|
+
delete loopPad.placement;
|
|
1922
|
+
}
|
|
1923
|
+
ack(sessionId, "pad_move", { playId, x, z });
|
|
1924
|
+
}
|
|
1761
1925
|
async function handleJoin(sessionId, demo, assetOrigin) {
|
|
1762
1926
|
teardownSession(sessionId);
|
|
1763
1927
|
const state = defaultSessionState(demo, assetOrigin);
|
|
@@ -1884,11 +2048,54 @@ async function dispatchMessage(sessionId, message) {
|
|
|
1884
2048
|
state.orbit.elevation = message.elevation;
|
|
1885
2049
|
}
|
|
1886
2050
|
if (message.paused !== void 0) {
|
|
2051
|
+
if (message.paused === false && state.orbitManualPose) {
|
|
2052
|
+
syncOrbitClockFromManualPose(state);
|
|
2053
|
+
}
|
|
1887
2054
|
state.orbit.paused = message.paused;
|
|
1888
2055
|
}
|
|
2056
|
+
if (message.frequencyHz !== void 0) {
|
|
2057
|
+
state.orbitSineFrequencyHz = message.frequencyHz;
|
|
2058
|
+
if (state.orbitSineEnabled && state.orbitSinePlayId) {
|
|
2059
|
+
void startOrbitSinePlay(sessionId, state);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (message.sinePlaying !== void 0) {
|
|
2063
|
+
state.orbitSineEnabled = message.sinePlaying;
|
|
2064
|
+
if (message.sinePlaying) {
|
|
2065
|
+
if (!state.orbitSinePlayId) {
|
|
2066
|
+
void startOrbitSinePlay(sessionId, state);
|
|
2067
|
+
}
|
|
2068
|
+
} else {
|
|
2069
|
+
void stopOrbitSine(state);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
1889
2072
|
ack(sessionId, "orbit_set");
|
|
1890
2073
|
return;
|
|
1891
2074
|
}
|
|
2075
|
+
if (message.action === "place") {
|
|
2076
|
+
state.orbit.paused = true;
|
|
2077
|
+
state.orbitManualPose = { x: message.x, z: message.z };
|
|
2078
|
+
const pose = orbitPoseFromXz(state, message.x, message.z);
|
|
2079
|
+
if (state.orbitSinePlayId) {
|
|
2080
|
+
void safeMixCall(
|
|
2081
|
+
sessionId,
|
|
2082
|
+
"setPlayPose",
|
|
2083
|
+
() => setPlayPose(state.orbitSinePlayId, pose)
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
void safeMixCall(
|
|
2087
|
+
sessionId,
|
|
2088
|
+
"setTtsPose",
|
|
2089
|
+
() => setTtsPose(sessionId, pose)
|
|
2090
|
+
);
|
|
2091
|
+
emitOrbitPlacePose(sessionId, state, message.x, message.z);
|
|
2092
|
+
ack(sessionId, "orbit_place");
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
sendToClient(sessionId, {
|
|
2096
|
+
type: "orbit_say_status",
|
|
2097
|
+
status: "playing"
|
|
2098
|
+
});
|
|
1892
2099
|
speak(sessionId, message.text);
|
|
1893
2100
|
ack(sessionId, "orbit_say");
|
|
1894
2101
|
return;
|
|
@@ -1945,6 +2152,20 @@ async function dispatchMessage(sessionId, message) {
|
|
|
1945
2152
|
}
|
|
1946
2153
|
return;
|
|
1947
2154
|
}
|
|
2155
|
+
case "pad_move": {
|
|
2156
|
+
if (state.demo !== "soundboard") {
|
|
2157
|
+
ackError(sessionId, "pad_move", "wrong_demo");
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
await handlePadMove(
|
|
2161
|
+
sessionId,
|
|
2162
|
+
state,
|
|
2163
|
+
message.playId,
|
|
2164
|
+
message.x,
|
|
2165
|
+
message.z
|
|
2166
|
+
);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
1948
2169
|
case "move": {
|
|
1949
2170
|
if (state.demo !== "proximity") {
|
|
1950
2171
|
ackError(sessionId, "move", "wrong_demo");
|
|
@@ -2027,6 +2248,23 @@ defineAgent({
|
|
|
2027
2248
|
}
|
|
2028
2249
|
void dispatchMessage(ctx.sessionId, message);
|
|
2029
2250
|
},
|
|
2251
|
+
onSpeechEvent(ctx, event) {
|
|
2252
|
+
const state = sessions.get(ctx.sessionId);
|
|
2253
|
+
if (!state || state.demo !== "orbit") {
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2256
|
+
if (event.type === "agent_speaking_start") {
|
|
2257
|
+
sendToClient(ctx.sessionId, {
|
|
2258
|
+
type: "orbit_say_status",
|
|
2259
|
+
status: "playing"
|
|
2260
|
+
});
|
|
2261
|
+
} else if (event.type === "agent_speaking_end") {
|
|
2262
|
+
sendToClient(ctx.sessionId, {
|
|
2263
|
+
type: "orbit_say_status",
|
|
2264
|
+
status: "ended"
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
},
|
|
2030
2268
|
onSessionEnd({ sessionId }) {
|
|
2031
2269
|
teardownSession(sessionId);
|
|
2032
2270
|
agentLog("info", `spatial-showcase session_end ${sessionId}`, sessionId);
|
|
@@ -2034,5 +2272,6 @@ defineAgent({
|
|
|
2034
2272
|
});
|
|
2035
2273
|
export {
|
|
2036
2274
|
MAX_ACTIVE_PLAYS,
|
|
2275
|
+
handlePadMove,
|
|
2037
2276
|
handlePadPlay
|
|
2038
2277
|
};
|
package/package.json
CHANGED
|
@@ -51,7 +51,9 @@ import {
|
|
|
51
51
|
} from "./sounds.js";
|
|
52
52
|
import {
|
|
53
53
|
buildOrbitSineInlineClipBase64,
|
|
54
|
+
ORBIT_SINE_DURATION_MS,
|
|
54
55
|
ORBIT_SINE_INLINE_URL,
|
|
56
|
+
orbitSineClipDurationMs,
|
|
55
57
|
} from "./sine.js";
|
|
56
58
|
|
|
57
59
|
const ORBIT_TICK_MS = 50;
|
|
@@ -59,6 +61,7 @@ const ORBIT_POSE_EMIT_MS = 100;
|
|
|
59
61
|
const ROOM_STATE_TICK_MS = 100;
|
|
60
62
|
const LOOP_PAD_POLL_MS = 250;
|
|
61
63
|
const ORBIT_SINE_POLL_MS = 250;
|
|
64
|
+
const ORBIT_SINE_GAPLESS_LEAD_MS = 200;
|
|
62
65
|
const ORBIT_SINE_VOLUME = 0.25;
|
|
63
66
|
export const MAX_ACTIVE_PLAYS = 12;
|
|
64
67
|
const SHOWCASE_ROOM_GROUP_ID = "showcase-room";
|
|
@@ -84,6 +87,11 @@ type SessionState = {
|
|
|
84
87
|
loopPollTimers: Map<string, ReturnType<typeof setInterval>>;
|
|
85
88
|
orbitSinePlayId?: string;
|
|
86
89
|
orbitSinePollTimer?: ReturnType<typeof setInterval>;
|
|
90
|
+
orbitSineRestartTimer?: ReturnType<typeof setTimeout>;
|
|
91
|
+
orbitSineFrequencyHz: number;
|
|
92
|
+
orbitSineEnabled: boolean;
|
|
93
|
+
orbitSinePhaseRad: number;
|
|
94
|
+
orbitManualPose?: { x: number; z: number };
|
|
87
95
|
inProximityRoom: boolean;
|
|
88
96
|
lastOrbitPoseEmitMs?: number;
|
|
89
97
|
};
|
|
@@ -175,6 +183,9 @@ function defaultSessionState(
|
|
|
175
183
|
playIds: new Set(),
|
|
176
184
|
loopPads: new Map(),
|
|
177
185
|
loopPollTimers: new Map(),
|
|
186
|
+
orbitSineFrequencyHz: 440,
|
|
187
|
+
orbitSineEnabled: true,
|
|
188
|
+
orbitSinePhaseRad: 0,
|
|
178
189
|
inProximityRoom: false,
|
|
179
190
|
};
|
|
180
191
|
}
|
|
@@ -229,6 +240,17 @@ function clearAllPlays(sessionId: string, state: SessionState): void {
|
|
|
229
240
|
}
|
|
230
241
|
|
|
231
242
|
function currentOrbitPose(state: SessionState) {
|
|
243
|
+
if (state.orbitManualPose) {
|
|
244
|
+
const elevation = state.orbit.elevation ?? 0;
|
|
245
|
+
return {
|
|
246
|
+
position: {
|
|
247
|
+
x: state.orbitManualPose.x,
|
|
248
|
+
y: elevation,
|
|
249
|
+
z: state.orbitManualPose.z,
|
|
250
|
+
},
|
|
251
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
232
254
|
if (!state.orbitStartMs) {
|
|
233
255
|
return orbitTtsPose(0, state.orbit);
|
|
234
256
|
}
|
|
@@ -236,6 +258,30 @@ function currentOrbitPose(state: SessionState) {
|
|
|
236
258
|
return orbitTtsPose(elapsedSec, state.orbit);
|
|
237
259
|
}
|
|
238
260
|
|
|
261
|
+
function orbitPoseFromXz(
|
|
262
|
+
state: SessionState,
|
|
263
|
+
x: number,
|
|
264
|
+
z: number,
|
|
265
|
+
): ReturnType<typeof orbitTtsPose> {
|
|
266
|
+
const elevation = state.orbit.elevation ?? 0;
|
|
267
|
+
return {
|
|
268
|
+
position: { x, y: elevation, z },
|
|
269
|
+
orientation: { x: 0, y: 0, z: 0, w: 1 },
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function syncOrbitClockFromManualPose(state: SessionState): void {
|
|
274
|
+
const manual = state.orbitManualPose;
|
|
275
|
+
if (!manual) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const periodSec = state.orbit.periodSec ?? 2 * Math.PI;
|
|
279
|
+
const angleRad = Math.atan2(manual.z, manual.x);
|
|
280
|
+
const elapsedSec = (angleRad * periodSec) / (2 * Math.PI);
|
|
281
|
+
state.orbitStartMs = Date.now() - elapsedSec * 1000;
|
|
282
|
+
state.orbitManualPose = undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
239
285
|
function clearOrbitSinePoll(state: SessionState): void {
|
|
240
286
|
if (state.orbitSinePollTimer) {
|
|
241
287
|
clearInterval(state.orbitSinePollTimer);
|
|
@@ -243,8 +289,16 @@ function clearOrbitSinePoll(state: SessionState): void {
|
|
|
243
289
|
}
|
|
244
290
|
}
|
|
245
291
|
|
|
292
|
+
function clearOrbitSineRestartTimer(state: SessionState): void {
|
|
293
|
+
if (state.orbitSineRestartTimer) {
|
|
294
|
+
clearTimeout(state.orbitSineRestartTimer);
|
|
295
|
+
state.orbitSineRestartTimer = undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
246
299
|
async function stopOrbitSine(state: SessionState): Promise<void> {
|
|
247
300
|
clearOrbitSinePoll(state);
|
|
301
|
+
clearOrbitSineRestartTimer(state);
|
|
248
302
|
const playId = state.orbitSinePlayId;
|
|
249
303
|
state.orbitSinePlayId = undefined;
|
|
250
304
|
if (playId) {
|
|
@@ -252,7 +306,68 @@ async function stopOrbitSine(state: SessionState): Promise<void> {
|
|
|
252
306
|
}
|
|
253
307
|
}
|
|
254
308
|
|
|
309
|
+
function orbitSineClipBytes(state: SessionState): string {
|
|
310
|
+
return buildOrbitSineInlineClipBase64(
|
|
311
|
+
ORBIT_SINE_DURATION_MS,
|
|
312
|
+
state.orbitSineFrequencyHz,
|
|
313
|
+
undefined,
|
|
314
|
+
state.orbitSinePhaseRad,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function orbitSineClipDuration(state: SessionState): number {
|
|
319
|
+
return orbitSineClipDurationMs(
|
|
320
|
+
ORBIT_SINE_DURATION_MS,
|
|
321
|
+
state.orbitSineFrequencyHz,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function scheduleOrbitSineGaplessRestart(
|
|
326
|
+
sessionId: string,
|
|
327
|
+
state: SessionState,
|
|
328
|
+
): void {
|
|
329
|
+
clearOrbitSineRestartTimer(state);
|
|
330
|
+
const durationMs = orbitSineClipDuration(state);
|
|
331
|
+
const delayMs = Math.max(0, durationMs - ORBIT_SINE_GAPLESS_LEAD_MS);
|
|
332
|
+
state.orbitSineRestartTimer = setTimeout(() => {
|
|
333
|
+
state.orbitSineRestartTimer = undefined;
|
|
334
|
+
void restartOrbitSineGapless(sessionId, state);
|
|
335
|
+
}, delayMs);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function restartOrbitSineGapless(
|
|
339
|
+
sessionId: string,
|
|
340
|
+
state: SessionState,
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
if (!state.orbitSineEnabled || !state.orbitSinePlayId) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const previousPlayId = state.orbitSinePlayId;
|
|
346
|
+
const result = await play({
|
|
347
|
+
url: ORBIT_SINE_INLINE_URL,
|
|
348
|
+
bytes: orbitSineClipBytes(state),
|
|
349
|
+
sessionIds: [sessionId],
|
|
350
|
+
volume: ORBIT_SINE_VOLUME,
|
|
351
|
+
pose: currentOrbitPose(state),
|
|
352
|
+
});
|
|
353
|
+
if (!result.ok || !result.playId) {
|
|
354
|
+
agentLog(
|
|
355
|
+
"warn",
|
|
356
|
+
`spatial-showcase orbit sine gapless restart failed: ${result.reason ?? "play_failed"}`,
|
|
357
|
+
sessionId,
|
|
358
|
+
);
|
|
359
|
+
startOrbitSineLoopPoll(sessionId, state);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
state.orbitSinePlayId = result.playId;
|
|
363
|
+
void stopPlay(previousPlayId);
|
|
364
|
+
scheduleOrbitSineGaplessRestart(sessionId, state);
|
|
365
|
+
}
|
|
366
|
+
|
|
255
367
|
function startOrbitSineLoopPoll(sessionId: string, state: SessionState): void {
|
|
368
|
+
if (state.orbitSineRestartTimer) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
256
371
|
clearOrbitSinePoll(state);
|
|
257
372
|
const playId = state.orbitSinePlayId;
|
|
258
373
|
if (!playId) {
|
|
@@ -261,7 +376,7 @@ function startOrbitSineLoopPoll(sessionId: string, state: SessionState): void {
|
|
|
261
376
|
|
|
262
377
|
state.orbitSinePollTimer = setInterval(() => {
|
|
263
378
|
void (async () => {
|
|
264
|
-
if (!state.orbitSinePlayId) {
|
|
379
|
+
if (!state.orbitSinePlayId || state.orbitSineRestartTimer) {
|
|
265
380
|
clearOrbitSinePoll(state);
|
|
266
381
|
return;
|
|
267
382
|
}
|
|
@@ -270,16 +385,7 @@ function startOrbitSineLoopPoll(sessionId: string, state: SessionState): void {
|
|
|
270
385
|
return;
|
|
271
386
|
}
|
|
272
387
|
if (status.status === "ended") {
|
|
273
|
-
|
|
274
|
-
url: ORBIT_SINE_INLINE_URL,
|
|
275
|
-
bytes: buildOrbitSineInlineClipBase64(),
|
|
276
|
-
sessionIds: [sessionId],
|
|
277
|
-
volume: ORBIT_SINE_VOLUME,
|
|
278
|
-
pose: currentOrbitPose(state),
|
|
279
|
-
});
|
|
280
|
-
if (replay.ok && replay.playId) {
|
|
281
|
-
state.orbitSinePlayId = replay.playId;
|
|
282
|
-
}
|
|
388
|
+
await restartOrbitSineGapless(sessionId, state);
|
|
283
389
|
}
|
|
284
390
|
})();
|
|
285
391
|
}, ORBIT_SINE_POLL_MS);
|
|
@@ -289,10 +395,13 @@ async function startOrbitSinePlay(
|
|
|
289
395
|
sessionId: string,
|
|
290
396
|
state: SessionState,
|
|
291
397
|
): Promise<void> {
|
|
398
|
+
if (!state.orbitSineEnabled) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
292
401
|
await stopOrbitSine(state);
|
|
293
402
|
const result = await play({
|
|
294
403
|
url: ORBIT_SINE_INLINE_URL,
|
|
295
|
-
bytes:
|
|
404
|
+
bytes: orbitSineClipBytes(state),
|
|
296
405
|
sessionIds: [sessionId],
|
|
297
406
|
volume: ORBIT_SINE_VOLUME,
|
|
298
407
|
pose: currentOrbitPose(state),
|
|
@@ -306,11 +415,12 @@ async function startOrbitSinePlay(
|
|
|
306
415
|
return;
|
|
307
416
|
}
|
|
308
417
|
state.orbitSinePlayId = result.playId;
|
|
418
|
+
scheduleOrbitSineGaplessRestart(sessionId, state);
|
|
309
419
|
startOrbitSineLoopPoll(sessionId, state);
|
|
310
420
|
}
|
|
311
421
|
|
|
312
422
|
function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
313
|
-
if (!state.orbitStartMs) {
|
|
423
|
+
if (!state.orbitStartMs && !state.orbitManualPose) {
|
|
314
424
|
return;
|
|
315
425
|
}
|
|
316
426
|
const now = Date.now();
|
|
@@ -321,10 +431,12 @@ function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
|
321
431
|
return;
|
|
322
432
|
}
|
|
323
433
|
state.lastOrbitPoseEmitMs = now;
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
(2 * Math.PI *
|
|
434
|
+
const pose = currentOrbitPose(state);
|
|
435
|
+
const angleRad = state.orbitManualPose
|
|
436
|
+
? Math.atan2(state.orbitManualPose.z, state.orbitManualPose.x)
|
|
437
|
+
: (2 * Math.PI * (now - state.orbitStartMs!)) /
|
|
438
|
+
1000 /
|
|
439
|
+
(state.orbit.periodSec ?? 2 * Math.PI);
|
|
328
440
|
sendToClient(sessionId, {
|
|
329
441
|
type: "orbit_pose",
|
|
330
442
|
angleDeg: (angleRad * 180) / Math.PI,
|
|
@@ -333,12 +445,28 @@ function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
|
333
445
|
});
|
|
334
446
|
}
|
|
335
447
|
|
|
448
|
+
function emitOrbitPlacePose(
|
|
449
|
+
sessionId: string,
|
|
450
|
+
state: SessionState,
|
|
451
|
+
x: number,
|
|
452
|
+
z: number,
|
|
453
|
+
): void {
|
|
454
|
+
state.lastOrbitPoseEmitMs = Date.now();
|
|
455
|
+
const angleRad = Math.atan2(z, x);
|
|
456
|
+
sendToClient(sessionId, {
|
|
457
|
+
type: "orbit_pose",
|
|
458
|
+
angleDeg: (angleRad * 180) / Math.PI,
|
|
459
|
+
x,
|
|
460
|
+
z,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
336
464
|
function startOrbitDemo(sessionId: string, state: SessionState): void {
|
|
337
465
|
stopOrbitInterval(state);
|
|
338
466
|
beginOrbitClock(state);
|
|
339
467
|
void startOrbitSinePlay(sessionId, state);
|
|
340
468
|
state.orbitTimer = setInterval(() => {
|
|
341
|
-
if (state.orbit.paused || !state.orbitStartMs) {
|
|
469
|
+
if (state.orbit.paused || (!state.orbitStartMs && !state.orbitManualPose)) {
|
|
342
470
|
return;
|
|
343
471
|
}
|
|
344
472
|
const pose = currentOrbitPose(state);
|
|
@@ -524,6 +652,34 @@ export async function handlePadPlay(
|
|
|
524
652
|
}
|
|
525
653
|
}
|
|
526
654
|
|
|
655
|
+
export async function handlePadMove(
|
|
656
|
+
sessionId: string,
|
|
657
|
+
state: SessionState,
|
|
658
|
+
playId: string,
|
|
659
|
+
x: number,
|
|
660
|
+
z: number,
|
|
661
|
+
): Promise<void> {
|
|
662
|
+
if (!state.playIds.has(playId)) {
|
|
663
|
+
ackError(sessionId, "pad_move", "unknown_play");
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const poseResult = await setPlayPose(playId, poseAt(x, z));
|
|
668
|
+
if (!poseResult.ok) {
|
|
669
|
+
ackError(sessionId, "pad_move", poseResult.reason ?? "set_pose_failed");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const loopPad = state.loopPads.get(playId);
|
|
674
|
+
if (loopPad) {
|
|
675
|
+
loopPad.x = x;
|
|
676
|
+
loopPad.z = z;
|
|
677
|
+
delete loopPad.placement;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
ack(sessionId, "pad_move", { playId, x, z });
|
|
681
|
+
}
|
|
682
|
+
|
|
527
683
|
async function handleJoin(
|
|
528
684
|
sessionId: string,
|
|
529
685
|
demo: ShowcaseDemo,
|
|
@@ -657,11 +813,50 @@ async function dispatchMessage(
|
|
|
657
813
|
state.orbit.elevation = message.elevation;
|
|
658
814
|
}
|
|
659
815
|
if (message.paused !== undefined) {
|
|
816
|
+
if (message.paused === false && state.orbitManualPose) {
|
|
817
|
+
syncOrbitClockFromManualPose(state);
|
|
818
|
+
}
|
|
660
819
|
state.orbit.paused = message.paused;
|
|
661
820
|
}
|
|
821
|
+
if (message.frequencyHz !== undefined) {
|
|
822
|
+
state.orbitSineFrequencyHz = message.frequencyHz;
|
|
823
|
+
if (state.orbitSineEnabled && state.orbitSinePlayId) {
|
|
824
|
+
void startOrbitSinePlay(sessionId, state);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (message.sinePlaying !== undefined) {
|
|
828
|
+
state.orbitSineEnabled = message.sinePlaying;
|
|
829
|
+
if (message.sinePlaying) {
|
|
830
|
+
if (!state.orbitSinePlayId) {
|
|
831
|
+
void startOrbitSinePlay(sessionId, state);
|
|
832
|
+
}
|
|
833
|
+
} else {
|
|
834
|
+
void stopOrbitSine(state);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
662
837
|
ack(sessionId, "orbit_set");
|
|
663
838
|
return;
|
|
664
839
|
}
|
|
840
|
+
if (message.action === "place") {
|
|
841
|
+
state.orbit.paused = true;
|
|
842
|
+
state.orbitManualPose = { x: message.x, z: message.z };
|
|
843
|
+
const pose = orbitPoseFromXz(state, message.x, message.z);
|
|
844
|
+
if (state.orbitSinePlayId) {
|
|
845
|
+
void safeMixCall(sessionId, "setPlayPose", () =>
|
|
846
|
+
setPlayPose(state.orbitSinePlayId!, pose),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
void safeMixCall(sessionId, "setTtsPose", () =>
|
|
850
|
+
setTtsPose(sessionId, pose),
|
|
851
|
+
);
|
|
852
|
+
emitOrbitPlacePose(sessionId, state, message.x, message.z);
|
|
853
|
+
ack(sessionId, "orbit_place");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
sendToClient(sessionId, {
|
|
857
|
+
type: "orbit_say_status",
|
|
858
|
+
status: "playing",
|
|
859
|
+
});
|
|
665
860
|
speak(sessionId, message.text);
|
|
666
861
|
ack(sessionId, "orbit_say");
|
|
667
862
|
return;
|
|
@@ -718,6 +913,20 @@ async function dispatchMessage(
|
|
|
718
913
|
}
|
|
719
914
|
return;
|
|
720
915
|
}
|
|
916
|
+
case "pad_move": {
|
|
917
|
+
if (state.demo !== "soundboard") {
|
|
918
|
+
ackError(sessionId, "pad_move", "wrong_demo");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
await handlePadMove(
|
|
922
|
+
sessionId,
|
|
923
|
+
state,
|
|
924
|
+
message.playId,
|
|
925
|
+
message.x,
|
|
926
|
+
message.z,
|
|
927
|
+
);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
721
930
|
case "move": {
|
|
722
931
|
if (state.demo !== "proximity") {
|
|
723
932
|
ackError(sessionId, "move", "wrong_demo");
|
|
@@ -800,6 +1009,24 @@ defineAgent({
|
|
|
800
1009
|
void dispatchMessage(ctx.sessionId, message);
|
|
801
1010
|
},
|
|
802
1011
|
|
|
1012
|
+
onSpeechEvent(ctx, event) {
|
|
1013
|
+
const state = sessions.get(ctx.sessionId);
|
|
1014
|
+
if (!state || state.demo !== "orbit") {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (event.type === "agent_speaking_start") {
|
|
1018
|
+
sendToClient(ctx.sessionId, {
|
|
1019
|
+
type: "orbit_say_status",
|
|
1020
|
+
status: "playing",
|
|
1021
|
+
});
|
|
1022
|
+
} else if (event.type === "agent_speaking_end") {
|
|
1023
|
+
sendToClient(ctx.sessionId, {
|
|
1024
|
+
type: "orbit_say_status",
|
|
1025
|
+
status: "ended",
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
|
|
803
1030
|
onSessionEnd({ sessionId }) {
|
|
804
1031
|
teardownSession(sessionId);
|
|
805
1032
|
agentLog("info", `spatial-showcase session_end ${sessionId}`, sessionId);
|
|
@@ -8,6 +8,8 @@ const SHOWCASE_DEMOS = new Set<string>(["orbit", "soundboard", "proximity"]);
|
|
|
8
8
|
|
|
9
9
|
export const MAX_SAY_TEXT_LENGTH = 200;
|
|
10
10
|
export const MAX_XZ = 5;
|
|
11
|
+
export const ORBIT_SINE_MIN_FREQUENCY_HZ = 110;
|
|
12
|
+
export const ORBIT_SINE_MAX_FREQUENCY_HZ = 1760;
|
|
11
13
|
|
|
12
14
|
export type ShowcaseJoinMessage = {
|
|
13
15
|
type: "join";
|
|
@@ -22,6 +24,8 @@ export type ShowcaseOrbitSetMessage = {
|
|
|
22
24
|
periodSec?: number;
|
|
23
25
|
elevation?: number;
|
|
24
26
|
paused?: boolean;
|
|
27
|
+
frequencyHz?: number;
|
|
28
|
+
sinePlaying?: boolean;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
export type ShowcaseOrbitSayMessage = {
|
|
@@ -30,6 +34,13 @@ export type ShowcaseOrbitSayMessage = {
|
|
|
30
34
|
text: string;
|
|
31
35
|
};
|
|
32
36
|
|
|
37
|
+
export type ShowcaseOrbitPlaceMessage = {
|
|
38
|
+
type: "orbit";
|
|
39
|
+
action: "place";
|
|
40
|
+
x: number;
|
|
41
|
+
z: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
33
44
|
export type ShowcasePadMessage = {
|
|
34
45
|
type: "pad";
|
|
35
46
|
clipId: string;
|
|
@@ -49,6 +60,13 @@ export type ShowcasePadStatusMessage = {
|
|
|
49
60
|
playId: string;
|
|
50
61
|
};
|
|
51
62
|
|
|
63
|
+
export type ShowcasePadMoveMessage = {
|
|
64
|
+
type: "pad_move";
|
|
65
|
+
playId: string;
|
|
66
|
+
x: number;
|
|
67
|
+
z: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
52
70
|
export type ShowcaseMoveMessage = {
|
|
53
71
|
type: "move";
|
|
54
72
|
x: number;
|
|
@@ -74,9 +92,11 @@ export type ShowcaseInbound =
|
|
|
74
92
|
| ShowcaseJoinMessage
|
|
75
93
|
| ShowcaseOrbitSetMessage
|
|
76
94
|
| ShowcaseOrbitSayMessage
|
|
95
|
+
| ShowcaseOrbitPlaceMessage
|
|
77
96
|
| ShowcasePadMessage
|
|
78
97
|
| ShowcasePadStopMessage
|
|
79
98
|
| ShowcasePadStatusMessage
|
|
99
|
+
| ShowcasePadMoveMessage
|
|
80
100
|
| ShowcaseMoveMessage
|
|
81
101
|
| ShowcaseMutePeerMessage
|
|
82
102
|
| ShowcaseLeaveMessage
|
|
@@ -98,6 +118,11 @@ export type ShowcaseOrbitPose = {
|
|
|
98
118
|
z: number;
|
|
99
119
|
};
|
|
100
120
|
|
|
121
|
+
export type ShowcaseOrbitSayStatus = {
|
|
122
|
+
type: "orbit_say_status";
|
|
123
|
+
status: "playing" | "ended";
|
|
124
|
+
};
|
|
125
|
+
|
|
101
126
|
export type ShowcasePadProgress = {
|
|
102
127
|
type: "pad_progress";
|
|
103
128
|
playId: string;
|
|
@@ -127,6 +152,7 @@ export type ShowcaseError = {
|
|
|
127
152
|
export type ShowcaseOutbound =
|
|
128
153
|
| ShowcaseAck
|
|
129
154
|
| ShowcaseOrbitPose
|
|
155
|
+
| ShowcaseOrbitSayStatus
|
|
130
156
|
| ShowcasePadProgress
|
|
131
157
|
| ShowcaseRoomState
|
|
132
158
|
| ShowcaseRoomFull
|
|
@@ -144,6 +170,12 @@ function isInXzRange(value: number): boolean {
|
|
|
144
170
|
return value >= -MAX_XZ && value <= MAX_XZ;
|
|
145
171
|
}
|
|
146
172
|
|
|
173
|
+
function isOrbitSineFrequencyHz(value: number): boolean {
|
|
174
|
+
return (
|
|
175
|
+
value >= ORBIT_SINE_MIN_FREQUENCY_HZ && value <= ORBIT_SINE_MAX_FREQUENCY_HZ
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
function isMixPlacement(value: unknown): value is MixPlacement {
|
|
148
180
|
return (
|
|
149
181
|
value === "center" ||
|
|
@@ -221,6 +253,19 @@ export function parseShowcaseMessage(message: unknown): ShowcaseInbound | null {
|
|
|
221
253
|
if (set.paused !== undefined && typeof set.paused !== "boolean") {
|
|
222
254
|
return null;
|
|
223
255
|
}
|
|
256
|
+
if (
|
|
257
|
+
set.frequencyHz !== undefined &&
|
|
258
|
+
(!isFiniteNumber(set.frequencyHz) ||
|
|
259
|
+
!isOrbitSineFrequencyHz(set.frequencyHz))
|
|
260
|
+
) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
if (
|
|
264
|
+
set.sinePlaying !== undefined &&
|
|
265
|
+
typeof set.sinePlaying !== "boolean"
|
|
266
|
+
) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
224
269
|
return {
|
|
225
270
|
type: "orbit",
|
|
226
271
|
action: "set",
|
|
@@ -228,8 +273,24 @@ export function parseShowcaseMessage(message: unknown): ShowcaseInbound | null {
|
|
|
228
273
|
...(set.periodSec !== undefined ? { periodSec: set.periodSec } : {}),
|
|
229
274
|
...(set.elevation !== undefined ? { elevation: set.elevation } : {}),
|
|
230
275
|
...(set.paused !== undefined ? { paused: set.paused } : {}),
|
|
276
|
+
...(set.frequencyHz !== undefined
|
|
277
|
+
? { frequencyHz: set.frequencyHz }
|
|
278
|
+
: {}),
|
|
279
|
+
...(set.sinePlaying !== undefined
|
|
280
|
+
? { sinePlaying: set.sinePlaying }
|
|
281
|
+
: {}),
|
|
231
282
|
};
|
|
232
283
|
}
|
|
284
|
+
if (orbit.action === "place") {
|
|
285
|
+
const place = message as ShowcaseOrbitPlaceMessage;
|
|
286
|
+
if (!isFiniteNumber(place.x) || !isInXzRange(place.x)) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
if (!isFiniteNumber(place.z) || !isInXzRange(place.z)) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return { type: "orbit", action: "place", x: place.x, z: place.z };
|
|
293
|
+
}
|
|
233
294
|
if (orbit.action === "say") {
|
|
234
295
|
const say = message as ShowcaseOrbitSayMessage;
|
|
235
296
|
if (
|
|
@@ -287,6 +348,24 @@ export function parseShowcaseMessage(message: unknown): ShowcaseInbound | null {
|
|
|
287
348
|
}
|
|
288
349
|
return { type: "pad_status", playId: status.playId.trim() };
|
|
289
350
|
}
|
|
351
|
+
case "pad_move": {
|
|
352
|
+
const padMove = message as ShowcasePadMoveMessage;
|
|
353
|
+
if (!isNonEmptyString(padMove.playId)) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
if (!isFiniteNumber(padMove.x) || !isInXzRange(padMove.x)) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (!isFiniteNumber(padMove.z) || !isInXzRange(padMove.z)) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
type: "pad_move",
|
|
364
|
+
playId: padMove.playId.trim(),
|
|
365
|
+
x: padMove.x,
|
|
366
|
+
z: padMove.z,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
290
369
|
case "move": {
|
|
291
370
|
const move = message as ShowcaseMoveMessage;
|
|
292
371
|
if (!isFiniteNumber(move.x) || !isInXzRange(move.x)) {
|
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
/** Inline orbit sine clip — 16 kHz mono s16le, decoded size under 64 KiB play cap. */
|
|
2
2
|
|
|
3
|
-
/** 2 s keeps loop-restart gaps
|
|
3
|
+
/** 2 s keeps loop-restart gaps infrequent; 16 kHz mono still fits under 64 KiB. */
|
|
4
4
|
export const ORBIT_SINE_DURATION_MS = 2000;
|
|
5
5
|
export const ORBIT_SINE_FREQUENCY_HZ = 440;
|
|
6
6
|
export const ORBIT_SINE_AMPLITUDE = 8000;
|
|
7
|
+
export const ORBIT_SINE_SAMPLE_RATE = 16_000;
|
|
7
8
|
export const ORBIT_SINE_INLINE_URL =
|
|
8
9
|
"https://example.com/inline-orbit-sine.wav";
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
+
/** Snap duration to an integer number of cycles so the clip loops at a zero crossing. */
|
|
12
|
+
export function orbitSineSampleCount(
|
|
13
|
+
durationMs = ORBIT_SINE_DURATION_MS,
|
|
14
|
+
frequencyHz = ORBIT_SINE_FREQUENCY_HZ,
|
|
15
|
+
sampleRate = ORBIT_SINE_SAMPLE_RATE,
|
|
16
|
+
): number {
|
|
17
|
+
const targetCycles = (durationMs / 1000) * frequencyHz;
|
|
18
|
+
const cycles = Math.max(1, Math.floor(targetCycles));
|
|
19
|
+
return Math.round((cycles * sampleRate) / frequencyHz);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Duration in ms for a clip built with {@link orbitSineSampleCount}. */
|
|
23
|
+
export function orbitSineClipDurationMs(
|
|
24
|
+
durationMs = ORBIT_SINE_DURATION_MS,
|
|
25
|
+
frequencyHz = ORBIT_SINE_FREQUENCY_HZ,
|
|
26
|
+
sampleRate = ORBIT_SINE_SAMPLE_RATE,
|
|
27
|
+
): number {
|
|
28
|
+
const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
|
|
29
|
+
return (numSamples / sampleRate) * 1000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Looping sine WAV as base64 for {@link play} inline bytes. */
|
|
11
33
|
export function buildOrbitSineInlineClipBase64(
|
|
12
34
|
durationMs = ORBIT_SINE_DURATION_MS,
|
|
13
35
|
frequencyHz = ORBIT_SINE_FREQUENCY_HZ,
|
|
14
36
|
amplitude = ORBIT_SINE_AMPLITUDE,
|
|
37
|
+
phaseRad = 0,
|
|
15
38
|
): string {
|
|
16
|
-
const sampleRate =
|
|
39
|
+
const sampleRate = ORBIT_SINE_SAMPLE_RATE;
|
|
17
40
|
const channels = 1;
|
|
18
41
|
const bytesPerSample = 2;
|
|
19
|
-
const numSamples =
|
|
42
|
+
const numSamples = orbitSineSampleCount(durationMs, frequencyHz, sampleRate);
|
|
20
43
|
const dataSize = numSamples * channels * bytesPerSample;
|
|
21
44
|
const buffer = Buffer.alloc(44 + dataSize);
|
|
22
45
|
buffer.write("RIFF", 0);
|
|
@@ -35,7 +58,7 @@ export function buildOrbitSineInlineClipBase64(
|
|
|
35
58
|
for (let i = 0; i < numSamples; i++) {
|
|
36
59
|
const t = i / sampleRate;
|
|
37
60
|
const sample = Math.round(
|
|
38
|
-
amplitude * Math.sin(2 * Math.PI * frequencyHz * t),
|
|
61
|
+
amplitude * Math.sin(2 * Math.PI * frequencyHz * t + phaseRad),
|
|
39
62
|
);
|
|
40
63
|
buffer.writeInt16LE(sample, 44 + i * 2);
|
|
41
64
|
}
|