@voicethere/agent 0.7.12 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +11 -2
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +1 -1
- package/dist/templates/echo-dc/agent.js +1 -1
- package/dist/templates/game-sync/agent.js +162 -125
- package/dist/templates/registry.d.ts +1 -1
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +42 -26
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/spatial-showcase/agent.js +261 -22
- package/dist/templates/voice-starter/agent.js +1 -1
- package/dist/templates/webhooks/agent.js +2 -2
- package/dist/templates/webhooks-redis/agent.js +11 -4
- package/dist/templates/world-sync/agent.js +829 -0
- package/dist/templates/world-sync-binary/agent.js +1031 -0
- package/package.json +3 -2
- package/templates/README.md +41 -69
- package/templates/crash/README.md +13 -0
- package/templates/echo/README.md +13 -0
- package/templates/{echo.ts → echo/agent.ts} +1 -1
- package/templates/echo-dc/README.md +13 -0
- package/templates/{echo-dc.ts → echo-dc/agent.ts} +1 -1
- package/templates/echo-smoke/README.md +13 -0
- package/templates/game-sync/README.md +21 -0
- package/templates/{game-sync.ts → game-sync/agent.ts} +126 -94
- package/templates/{game-sync-redis.ts → game-sync/redis.ts} +4 -13
- package/templates/{game-sync-sim.ts → game-sync/sim.ts} +14 -7
- package/templates/{game-sync-world-layout.ts → game-sync/world-layout.ts} +72 -30
- package/templates/game-sync-smoke/README.md +13 -0
- package/templates/mix-smoke/README.md +13 -0
- package/templates/positional-tts/README.md +13 -0
- package/templates/recording-consent/README.md +13 -0
- package/templates/redis-sync/README.md +23 -0
- package/templates/redis-sync/agent.ts +72 -95
- package/templates/redis-sync/world-layout.ts +103 -26
- package/templates/spatial-showcase/README.md +13 -0
- package/templates/spatial-showcase/agent.ts +245 -18
- package/templates/spatial-showcase/protocol.ts +79 -0
- package/templates/spatial-showcase/sine.ts +28 -5
- package/templates/voice-showcase/README.md +13 -0
- package/templates/voice-starter/README.md +13 -0
- package/templates/webhooks/README.md +13 -0
- package/templates/{webhooks.ts → webhooks/agent.ts} +1 -1
- package/templates/webhooks-redis/README.md +13 -0
- package/templates/{webhooks-redis.ts → webhooks-redis/agent.ts} +9 -9
- package/templates/world-sync/README.md +20 -0
- package/templates/world-sync/agent.ts +62 -0
- package/templates/world-sync-binary/README.md +21 -0
- package/templates/world-sync-binary/agent.ts +107 -0
- package/templates/world-sync-binary/protocol.ts +281 -0
- /package/templates/{crash.ts → crash/agent.ts} +0 -0
- /package/templates/{echo-smoke.ts → echo-smoke/agent.ts} +0 -0
- /package/templates/{game-sync-protocol.ts → game-sync/protocol.ts} +0 -0
- /package/templates/{game-sync-smoke.ts → game-sync-smoke/agent.ts} +0 -0
- /package/templates/{mix-smoke.ts → mix-smoke/agent.ts} +0 -0
- /package/templates/{agent.ts → voice-starter/agent.ts} +0 -0
|
@@ -18,8 +18,37 @@ export const PEER_FIELD_X = 1;
|
|
|
18
18
|
export const PEER_FIELD_Y = 2;
|
|
19
19
|
export const PEER_FIELD_ACTIVE = 3;
|
|
20
20
|
|
|
21
|
+
/** Client → agent position frame: float32le [clientIndex, x, y] (12 bytes). */
|
|
22
|
+
export const POSITION_FLOAT_COUNT = 3;
|
|
23
|
+
export const POSITION_BYTE_LENGTH = POSITION_FLOAT_COUNT * 4;
|
|
24
|
+
|
|
25
|
+
export type PeerPosition = {
|
|
26
|
+
clientIndex: number;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
21
31
|
export const REDIS_WORLD_KEY = "e2e:redis-sync:world";
|
|
22
32
|
|
|
33
|
+
/** Atomic splice of one peer slot (16 bytes) into the world blob. */
|
|
34
|
+
export const LUA_PATCH_PEER_SLOT = `
|
|
35
|
+
local key = KEYS[1]
|
|
36
|
+
local offset = tonumber(ARGV[1])
|
|
37
|
+
local slot = ARGV[2]
|
|
38
|
+
local size = tonumber(ARGV[3])
|
|
39
|
+
local world = redis.call('GET', key)
|
|
40
|
+
if not world then
|
|
41
|
+
world = string.rep(string.char(0), size)
|
|
42
|
+
elseif #world < size then
|
|
43
|
+
world = world .. string.rep(string.char(0), size - #world)
|
|
44
|
+
elseif #world > size then
|
|
45
|
+
world = string.sub(world, 1, size)
|
|
46
|
+
end
|
|
47
|
+
world = string.sub(world, 1, offset) .. slot .. string.sub(world, offset + #slot + 1)
|
|
48
|
+
redis.call('SET', key, world)
|
|
49
|
+
return size
|
|
50
|
+
`;
|
|
51
|
+
|
|
23
52
|
export type PeerSlot = {
|
|
24
53
|
clientIndex: number;
|
|
25
54
|
x: number;
|
|
@@ -53,6 +82,51 @@ export function clearPeerSlot(world: Float32Array, clientIndex: number): void {
|
|
|
53
82
|
writePeerSlot(world, clientIndex, 0, 0, 0);
|
|
54
83
|
}
|
|
55
84
|
|
|
85
|
+
function asDataView(data: ArrayBufferLike | ArrayBufferView): DataView {
|
|
86
|
+
if (ArrayBuffer.isView(data)) {
|
|
87
|
+
return new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
88
|
+
}
|
|
89
|
+
return new DataView(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Decode a 12-byte inbound position frame. */
|
|
93
|
+
export function decodePositionBuffer(
|
|
94
|
+
data: ArrayBufferLike | ArrayBufferView | null | undefined,
|
|
95
|
+
): PeerPosition | null {
|
|
96
|
+
if (data == null) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const view = asDataView(data);
|
|
100
|
+
if (view.byteLength < POSITION_BYTE_LENGTH) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const clientIndex = view.getFloat32(0, true);
|
|
104
|
+
const x = view.getFloat32(4, true);
|
|
105
|
+
const y = view.getFloat32(8, true);
|
|
106
|
+
if (
|
|
107
|
+
!Number.isFinite(clientIndex) ||
|
|
108
|
+
clientIndex < 0 ||
|
|
109
|
+
!Number.isFinite(x) ||
|
|
110
|
+
!Number.isFinite(y)
|
|
111
|
+
) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return { clientIndex: Math.trunc(clientIndex), x, y };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Write [clientIndex, x, y] into `dest` (length >= 3) and return it. */
|
|
118
|
+
export function encodePositionInto(
|
|
119
|
+
dest: Float32Array,
|
|
120
|
+
clientIndex: number,
|
|
121
|
+
x: number,
|
|
122
|
+
y: number,
|
|
123
|
+
): Float32Array {
|
|
124
|
+
dest[0] = clientIndex;
|
|
125
|
+
dest[1] = x;
|
|
126
|
+
dest[2] = y;
|
|
127
|
+
return dest;
|
|
128
|
+
}
|
|
129
|
+
|
|
56
130
|
export function readPeerSlot(
|
|
57
131
|
world: Float32Array,
|
|
58
132
|
clientIndex: number,
|
|
@@ -71,46 +145,49 @@ export function readPeerSlot(
|
|
|
71
145
|
}
|
|
72
146
|
|
|
73
147
|
/**
|
|
74
|
-
*
|
|
148
|
+
* Copy Redis / Node Buffer bytes into `dest` (default: a new world).
|
|
75
149
|
*
|
|
76
|
-
* Node Buffer pools
|
|
77
|
-
*
|
|
78
|
-
* "start offset of Float32Array should be a multiple of 4"
|
|
79
|
-
* Copy via `ArrayBuffer.slice` so the view always starts at offset 0.
|
|
150
|
+
* Node Buffer pools can have `byteOffset` not divisible by 4, so we copy via
|
|
151
|
+
* a Uint8Array view onto `dest` instead of slicing a new ArrayBuffer.
|
|
80
152
|
*/
|
|
81
153
|
export function normalizeWorldBuffer(
|
|
82
154
|
raw: Uint8Array | null | undefined,
|
|
155
|
+
dest: Float32Array = createEmptyWorldBuffer(),
|
|
83
156
|
): Float32Array {
|
|
157
|
+
const destBytes = new Uint8Array(
|
|
158
|
+
dest.buffer,
|
|
159
|
+
dest.byteOffset,
|
|
160
|
+
dest.byteLength,
|
|
161
|
+
);
|
|
84
162
|
if (!raw || raw.byteLength === 0) {
|
|
85
|
-
|
|
163
|
+
destBytes.fill(0);
|
|
164
|
+
return dest;
|
|
86
165
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
166
|
+
const byteCount = Math.min(
|
|
167
|
+
Math.floor(raw.byteLength / 4) * 4,
|
|
168
|
+
destBytes.byteLength,
|
|
169
|
+
);
|
|
170
|
+
if (byteCount > 0) {
|
|
171
|
+
destBytes.set(raw.subarray(0, byteCount));
|
|
90
172
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (decoded.length === WORLD_FLOAT_COUNT) {
|
|
94
|
-
return decoded;
|
|
173
|
+
if (byteCount < destBytes.byteLength) {
|
|
174
|
+
destBytes.fill(0, byteCount);
|
|
95
175
|
}
|
|
96
|
-
|
|
97
|
-
normalized.set(
|
|
98
|
-
decoded.subarray(0, Math.min(decoded.length, WORLD_FLOAT_COUNT)),
|
|
99
|
-
);
|
|
100
|
-
return normalized;
|
|
176
|
+
return dest;
|
|
101
177
|
}
|
|
102
178
|
|
|
103
|
-
export function decodeWorldBuffer(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const normalized = createEmptyWorldBuffer();
|
|
109
|
-
normalized.set(view.subarray(0, Math.min(view.length, WORLD_FLOAT_COUNT)));
|
|
110
|
-
return normalized;
|
|
179
|
+
export function decodeWorldBuffer(
|
|
180
|
+
data: ArrayBuffer,
|
|
181
|
+
dest: Float32Array = createEmptyWorldBuffer(),
|
|
182
|
+
): Float32Array {
|
|
183
|
+
return normalizeWorldBuffer(new Uint8Array(data), dest);
|
|
111
184
|
}
|
|
112
185
|
|
|
186
|
+
/** Same backing ArrayBuffer when the world is a standalone Float32Array. */
|
|
113
187
|
export function worldBufferToArrayBuffer(world: Float32Array): ArrayBuffer {
|
|
188
|
+
if (world.byteOffset === 0 && world.byteLength === world.buffer.byteLength) {
|
|
189
|
+
return world.buffer as ArrayBuffer;
|
|
190
|
+
}
|
|
114
191
|
return world.buffer.slice(
|
|
115
192
|
world.byteOffset,
|
|
116
193
|
world.byteOffset + world.byteLength,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# spatial-showcase
|
|
2
|
+
|
|
3
|
+
Spatial audio showcase for `/showcase`. A `{ type: "join", demo }` DataChannel message selects `orbit`, `soundboard`, or `proximity`.
|
|
4
|
+
|
|
5
|
+
## Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @voicethere/agent build --entry templates/spatial-showcase/agent.ts --outfile dist/agent.js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Sources
|
|
12
|
+
|
|
13
|
+
- `agent.ts`, `protocol.ts`, `sounds.ts`, `room.ts`, `orbit.ts`, `orbit-session.ts`, `sine.ts`
|
|
@@ -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);
|