@voicethere/agent 0.7.11 → 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.
- package/dist/agent.js +24 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +24 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +68 -1
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +26 -2
- package/dist/templates/echo-dc/agent.js +26 -2
- package/dist/templates/game-sync/agent.js +26 -2
- package/dist/templates/positional-tts/agent.js +26 -2
- package/dist/templates/recording-consent/agent.js +26 -2
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +2 -1
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/spatial-showcase/agent.js +498 -29
- package/dist/templates/voice-showcase/agent.js +26 -2
- package/dist/templates/voice-starter/agent.js +26 -2
- package/dist/templates/webhooks/agent.js +26 -2
- package/dist/templates/webhooks-redis/agent.js +26 -2
- package/package.json +4 -4
- package/templates/README.md +10 -10
- package/templates/spatial-showcase/agent.ts +404 -34
- package/templates/spatial-showcase/protocol.ts +82 -1
- package/templates/spatial-showcase/sine.ts +66 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spatial audio showcase —
|
|
2
|
+
* Spatial audio showcase — orbiting sine + TTS, positional soundboard, proximity room.
|
|
3
3
|
*
|
|
4
4
|
* Browser joins with `{ type: "join", demo, assetOrigin? }` then sends demo-specific
|
|
5
5
|
* commands. Clip URLs are resolved server-side from clipId + allowlisted assetOrigin.
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
sendToClient,
|
|
22
22
|
setClientPose,
|
|
23
23
|
setListenerMute,
|
|
24
|
+
setPlayPose,
|
|
24
25
|
setPositionalMixing,
|
|
26
|
+
setSttEnabled,
|
|
25
27
|
setTtsPose,
|
|
26
28
|
speak,
|
|
27
29
|
stopPlay,
|
|
@@ -42,16 +44,26 @@ import {
|
|
|
42
44
|
} from "./protocol.js";
|
|
43
45
|
import { ProximityRoom } from "./room.js";
|
|
44
46
|
import {
|
|
47
|
+
isShowcaseClipId,
|
|
45
48
|
resolveClipUrl,
|
|
46
49
|
SHOWCASE_SOUND_FILES,
|
|
47
50
|
type ShowcaseClipId,
|
|
48
51
|
} from "./sounds.js";
|
|
52
|
+
import {
|
|
53
|
+
buildOrbitSineInlineClipBase64,
|
|
54
|
+
ORBIT_SINE_DURATION_MS,
|
|
55
|
+
ORBIT_SINE_INLINE_URL,
|
|
56
|
+
orbitSineClipDurationMs,
|
|
57
|
+
} from "./sine.js";
|
|
49
58
|
|
|
50
59
|
const ORBIT_TICK_MS = 50;
|
|
51
60
|
const ORBIT_POSE_EMIT_MS = 100;
|
|
52
61
|
const ROOM_STATE_TICK_MS = 100;
|
|
53
62
|
const LOOP_PAD_POLL_MS = 250;
|
|
54
|
-
const
|
|
63
|
+
const ORBIT_SINE_POLL_MS = 250;
|
|
64
|
+
const ORBIT_SINE_GAPLESS_LEAD_MS = 200;
|
|
65
|
+
const ORBIT_SINE_VOLUME = 0.25;
|
|
66
|
+
export const MAX_ACTIVE_PLAYS = 12;
|
|
55
67
|
const SHOWCASE_ROOM_GROUP_ID = "showcase-room";
|
|
56
68
|
|
|
57
69
|
type LoopPadState = {
|
|
@@ -73,6 +85,13 @@ type SessionState = {
|
|
|
73
85
|
playIds: Set<string>;
|
|
74
86
|
loopPads: Map<string, LoopPadState>;
|
|
75
87
|
loopPollTimers: Map<string, ReturnType<typeof setInterval>>;
|
|
88
|
+
orbitSinePlayId?: string;
|
|
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 };
|
|
76
95
|
inProximityRoom: boolean;
|
|
77
96
|
lastOrbitPoseEmitMs?: number;
|
|
78
97
|
};
|
|
@@ -95,11 +114,15 @@ function ensureShowcaseRoomGroup(): Promise<boolean> {
|
|
|
95
114
|
return showcaseRoomGroupReady;
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
117
|
+
type PadAckExtra = {
|
|
118
|
+
playId?: string;
|
|
119
|
+
clipId?: ShowcaseClipId;
|
|
120
|
+
x?: number;
|
|
121
|
+
z?: number;
|
|
122
|
+
placement?: PlayOptions["placement"];
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function ack(sessionId: string, action: string, extra?: PadAckExtra): void {
|
|
103
126
|
sendToClient(sessionId, {
|
|
104
127
|
type: "showcase_ack",
|
|
105
128
|
action,
|
|
@@ -108,12 +131,18 @@ function ack(
|
|
|
108
131
|
});
|
|
109
132
|
}
|
|
110
133
|
|
|
111
|
-
function ackError(
|
|
134
|
+
function ackError(
|
|
135
|
+
sessionId: string,
|
|
136
|
+
action: string,
|
|
137
|
+
error: string,
|
|
138
|
+
extra?: { clipId?: ShowcaseClipId },
|
|
139
|
+
): void {
|
|
112
140
|
sendToClient(sessionId, {
|
|
113
141
|
type: "showcase_ack",
|
|
114
142
|
action,
|
|
115
143
|
ok: false,
|
|
116
144
|
error,
|
|
145
|
+
...(extra?.clipId !== undefined ? { clipId: extra.clipId } : {}),
|
|
117
146
|
});
|
|
118
147
|
}
|
|
119
148
|
|
|
@@ -154,6 +183,9 @@ function defaultSessionState(
|
|
|
154
183
|
playIds: new Set(),
|
|
155
184
|
loopPads: new Map(),
|
|
156
185
|
loopPollTimers: new Map(),
|
|
186
|
+
orbitSineFrequencyHz: 440,
|
|
187
|
+
orbitSineEnabled: true,
|
|
188
|
+
orbitSinePhaseRad: 0,
|
|
157
189
|
inProximityRoom: false,
|
|
158
190
|
};
|
|
159
191
|
}
|
|
@@ -165,6 +197,19 @@ function clearRoomStateTimer(state: SessionState): void {
|
|
|
165
197
|
}
|
|
166
198
|
}
|
|
167
199
|
|
|
200
|
+
function stopLoopPadsForClip(
|
|
201
|
+
sessionId: string,
|
|
202
|
+
state: SessionState,
|
|
203
|
+
clipId: ShowcaseClipId,
|
|
204
|
+
): void {
|
|
205
|
+
for (const [playId, pad] of [...state.loopPads.entries()]) {
|
|
206
|
+
if (pad.clipId === clipId) {
|
|
207
|
+
void stopPlay(playId);
|
|
208
|
+
clearLoopPad(sessionId, state, playId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
168
213
|
function clearLoopPad(
|
|
169
214
|
sessionId: string,
|
|
170
215
|
state: SessionState,
|
|
@@ -194,8 +239,188 @@ function clearAllPlays(sessionId: string, state: SessionState): void {
|
|
|
194
239
|
state.loopPollTimers.clear();
|
|
195
240
|
}
|
|
196
241
|
|
|
197
|
-
function
|
|
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
|
+
}
|
|
198
254
|
if (!state.orbitStartMs) {
|
|
255
|
+
return orbitTtsPose(0, state.orbit);
|
|
256
|
+
}
|
|
257
|
+
const elapsedSec = (Date.now() - state.orbitStartMs) / 1000;
|
|
258
|
+
return orbitTtsPose(elapsedSec, state.orbit);
|
|
259
|
+
}
|
|
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
|
+
|
|
285
|
+
function clearOrbitSinePoll(state: SessionState): void {
|
|
286
|
+
if (state.orbitSinePollTimer) {
|
|
287
|
+
clearInterval(state.orbitSinePollTimer);
|
|
288
|
+
state.orbitSinePollTimer = undefined;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function clearOrbitSineRestartTimer(state: SessionState): void {
|
|
293
|
+
if (state.orbitSineRestartTimer) {
|
|
294
|
+
clearTimeout(state.orbitSineRestartTimer);
|
|
295
|
+
state.orbitSineRestartTimer = undefined;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function stopOrbitSine(state: SessionState): Promise<void> {
|
|
300
|
+
clearOrbitSinePoll(state);
|
|
301
|
+
clearOrbitSineRestartTimer(state);
|
|
302
|
+
const playId = state.orbitSinePlayId;
|
|
303
|
+
state.orbitSinePlayId = undefined;
|
|
304
|
+
if (playId) {
|
|
305
|
+
await stopPlay(playId);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
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
|
+
|
|
367
|
+
function startOrbitSineLoopPoll(sessionId: string, state: SessionState): void {
|
|
368
|
+
if (state.orbitSineRestartTimer) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
clearOrbitSinePoll(state);
|
|
372
|
+
const playId = state.orbitSinePlayId;
|
|
373
|
+
if (!playId) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
state.orbitSinePollTimer = setInterval(() => {
|
|
378
|
+
void (async () => {
|
|
379
|
+
if (!state.orbitSinePlayId || state.orbitSineRestartTimer) {
|
|
380
|
+
clearOrbitSinePoll(state);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const status = await getPlay(state.orbitSinePlayId);
|
|
384
|
+
if (!status.ok) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (status.status === "ended") {
|
|
388
|
+
await restartOrbitSineGapless(sessionId, state);
|
|
389
|
+
}
|
|
390
|
+
})();
|
|
391
|
+
}, ORBIT_SINE_POLL_MS);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function startOrbitSinePlay(
|
|
395
|
+
sessionId: string,
|
|
396
|
+
state: SessionState,
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
if (!state.orbitSineEnabled) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
await stopOrbitSine(state);
|
|
402
|
+
const result = await play({
|
|
403
|
+
url: ORBIT_SINE_INLINE_URL,
|
|
404
|
+
bytes: orbitSineClipBytes(state),
|
|
405
|
+
sessionIds: [sessionId],
|
|
406
|
+
volume: ORBIT_SINE_VOLUME,
|
|
407
|
+
pose: currentOrbitPose(state),
|
|
408
|
+
});
|
|
409
|
+
if (!result.ok || !result.playId) {
|
|
410
|
+
agentLog(
|
|
411
|
+
"warn",
|
|
412
|
+
`spatial-showcase orbit sine play failed: ${result.reason ?? "play_failed"}`,
|
|
413
|
+
sessionId,
|
|
414
|
+
);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
state.orbitSinePlayId = result.playId;
|
|
418
|
+
scheduleOrbitSineGaplessRestart(sessionId, state);
|
|
419
|
+
startOrbitSineLoopPoll(sessionId, state);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
423
|
+
if (!state.orbitStartMs && !state.orbitManualPose) {
|
|
199
424
|
return;
|
|
200
425
|
}
|
|
201
426
|
const now = Date.now();
|
|
@@ -206,10 +431,12 @@ function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
|
206
431
|
return;
|
|
207
432
|
}
|
|
208
433
|
state.lastOrbitPoseEmitMs = now;
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
(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);
|
|
213
440
|
sendToClient(sessionId, {
|
|
214
441
|
type: "orbit_pose",
|
|
215
442
|
angleDeg: (angleRad * 180) / Math.PI,
|
|
@@ -218,16 +445,38 @@ function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
|
218
445
|
});
|
|
219
446
|
}
|
|
220
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
|
+
|
|
221
464
|
function startOrbitDemo(sessionId: string, state: SessionState): void {
|
|
222
465
|
stopOrbitInterval(state);
|
|
223
466
|
beginOrbitClock(state);
|
|
467
|
+
void startOrbitSinePlay(sessionId, state);
|
|
224
468
|
state.orbitTimer = setInterval(() => {
|
|
225
|
-
if (state.orbit.paused || !state.orbitStartMs) {
|
|
469
|
+
if (state.orbit.paused || (!state.orbitStartMs && !state.orbitManualPose)) {
|
|
226
470
|
return;
|
|
227
471
|
}
|
|
228
|
-
const
|
|
472
|
+
const pose = currentOrbitPose(state);
|
|
473
|
+
if (state.orbitSinePlayId) {
|
|
474
|
+
void safeMixCall(sessionId, "setPlayPose", () =>
|
|
475
|
+
setPlayPose(state.orbitSinePlayId!, pose),
|
|
476
|
+
);
|
|
477
|
+
}
|
|
229
478
|
void safeMixCall(sessionId, "setTtsPose", () =>
|
|
230
|
-
setTtsPose(sessionId,
|
|
479
|
+
setTtsPose(sessionId, pose),
|
|
231
480
|
);
|
|
232
481
|
emitOrbitPose(sessionId, state);
|
|
233
482
|
}, ORBIT_TICK_MS);
|
|
@@ -312,6 +561,15 @@ async function startLoopPadPoll(
|
|
|
312
561
|
clearLoopPad(sessionId, state, playId);
|
|
313
562
|
state.playIds.add(replay.playId);
|
|
314
563
|
state.loopPads.set(replay.playId, pad);
|
|
564
|
+
ack(sessionId, "pad", {
|
|
565
|
+
playId: replay.playId,
|
|
566
|
+
clipId: pad.clipId,
|
|
567
|
+
x: pad.x,
|
|
568
|
+
z: pad.z,
|
|
569
|
+
...(pad.placement !== undefined
|
|
570
|
+
? { placement: pad.placement }
|
|
571
|
+
: {}),
|
|
572
|
+
});
|
|
315
573
|
void startLoopPadPoll(sessionId, state, replay.playId, pad);
|
|
316
574
|
}
|
|
317
575
|
} else if (status.status) {
|
|
@@ -336,24 +594,27 @@ export async function handlePadPlay(
|
|
|
336
594
|
volume?: number,
|
|
337
595
|
placement?: PlayOptions["placement"],
|
|
338
596
|
): Promise<void> {
|
|
339
|
-
if (state.playIds.size >= MAX_ACTIVE_PLAYS) {
|
|
340
|
-
ackError(sessionId, "pad", "busy");
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
597
|
const assetOrigin = state.assetOrigin;
|
|
345
598
|
if (!assetOrigin) {
|
|
346
|
-
ackError(sessionId, "pad", "missing_asset_origin");
|
|
599
|
+
ackError(sessionId, "pad", "missing_asset_origin", { clipId });
|
|
347
600
|
return;
|
|
348
601
|
}
|
|
349
602
|
|
|
350
603
|
const url = resolveClipUrl(assetOrigin, clipId);
|
|
351
604
|
if (!url) {
|
|
352
|
-
ackError(sessionId, "pad", "invalid_asset_origin");
|
|
605
|
+
ackError(sessionId, "pad", "invalid_asset_origin", { clipId });
|
|
353
606
|
return;
|
|
354
607
|
}
|
|
355
608
|
|
|
356
609
|
const sound = SHOWCASE_SOUND_FILES[clipId];
|
|
610
|
+
if (sound.loop) {
|
|
611
|
+
stopLoopPadsForClip(sessionId, state, clipId);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (state.playIds.size >= MAX_ACTIVE_PLAYS) {
|
|
615
|
+
ackError(sessionId, "pad", "busy", { clipId });
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
357
618
|
const playVolume = volume ?? sound.defaultVolume;
|
|
358
619
|
const options: PlayOptions = {
|
|
359
620
|
url,
|
|
@@ -364,12 +625,18 @@ export async function handlePadPlay(
|
|
|
364
625
|
|
|
365
626
|
const result = await play(options);
|
|
366
627
|
if (!result.ok || !result.playId) {
|
|
367
|
-
ackError(sessionId, "pad", result.reason ?? "play_failed");
|
|
628
|
+
ackError(sessionId, "pad", result.reason ?? "play_failed", { clipId });
|
|
368
629
|
return;
|
|
369
630
|
}
|
|
370
631
|
|
|
371
632
|
state.playIds.add(result.playId);
|
|
372
|
-
ack(sessionId, "pad", {
|
|
633
|
+
ack(sessionId, "pad", {
|
|
634
|
+
playId: result.playId,
|
|
635
|
+
clipId,
|
|
636
|
+
x,
|
|
637
|
+
z,
|
|
638
|
+
...(placement !== undefined ? { placement } : {}),
|
|
639
|
+
});
|
|
373
640
|
|
|
374
641
|
if (sound.loop) {
|
|
375
642
|
const pad: LoopPadState = {
|
|
@@ -385,6 +652,34 @@ export async function handlePadPlay(
|
|
|
385
652
|
}
|
|
386
653
|
}
|
|
387
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
|
+
|
|
388
683
|
async function handleJoin(
|
|
389
684
|
sessionId: string,
|
|
390
685
|
demo: ShowcaseDemo,
|
|
@@ -406,9 +701,13 @@ async function handleJoin(
|
|
|
406
701
|
ack(sessionId, "join");
|
|
407
702
|
|
|
408
703
|
switch (demo) {
|
|
409
|
-
case "orbit":
|
|
704
|
+
case "orbit": {
|
|
705
|
+
void safeMixCall(sessionId, "setSttEnabled", () =>
|
|
706
|
+
setSttEnabled({ enabled: false, sessionId }),
|
|
707
|
+
);
|
|
410
708
|
startOrbitDemo(sessionId, state);
|
|
411
709
|
break;
|
|
710
|
+
}
|
|
412
711
|
case "soundboard":
|
|
413
712
|
break;
|
|
414
713
|
case "proximity": {
|
|
@@ -451,8 +750,12 @@ function teardownSession(sessionId: string): void {
|
|
|
451
750
|
|
|
452
751
|
resetOrbitSession(state);
|
|
453
752
|
clearRoomStateTimer(state);
|
|
753
|
+
void stopOrbitSine(state);
|
|
454
754
|
clearAllPlays(sessionId, state);
|
|
455
755
|
void safeMixCall(sessionId, "clearTtsPose", () => clearTtsPose(sessionId));
|
|
756
|
+
void safeMixCall(sessionId, "setSttEnabled", () =>
|
|
757
|
+
setSttEnabled({ enabled: true, sessionId }),
|
|
758
|
+
);
|
|
456
759
|
|
|
457
760
|
if (state.inProximityRoom) {
|
|
458
761
|
proximityRoom.leave(sessionId);
|
|
@@ -510,11 +813,50 @@ async function dispatchMessage(
|
|
|
510
813
|
state.orbit.elevation = message.elevation;
|
|
511
814
|
}
|
|
512
815
|
if (message.paused !== undefined) {
|
|
816
|
+
if (message.paused === false && state.orbitManualPose) {
|
|
817
|
+
syncOrbitClockFromManualPose(state);
|
|
818
|
+
}
|
|
513
819
|
state.orbit.paused = message.paused;
|
|
514
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
|
+
}
|
|
515
837
|
ack(sessionId, "orbit_set");
|
|
516
838
|
return;
|
|
517
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
|
+
});
|
|
518
860
|
speak(sessionId, message.text);
|
|
519
861
|
ack(sessionId, "orbit_say");
|
|
520
862
|
return;
|
|
@@ -524,6 +866,10 @@ async function dispatchMessage(
|
|
|
524
866
|
ackError(sessionId, "pad", "wrong_demo");
|
|
525
867
|
return;
|
|
526
868
|
}
|
|
869
|
+
if (!isShowcaseClipId(message.clipId)) {
|
|
870
|
+
ackError(sessionId, "pad", "invalid_clip_id");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
527
873
|
await handlePadPlay(
|
|
528
874
|
sessionId,
|
|
529
875
|
state,
|
|
@@ -567,6 +913,20 @@ async function dispatchMessage(
|
|
|
567
913
|
}
|
|
568
914
|
return;
|
|
569
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
|
+
}
|
|
570
930
|
case "move": {
|
|
571
931
|
if (state.demo !== "proximity") {
|
|
572
932
|
ackError(sessionId, "move", "wrong_demo");
|
|
@@ -641,14 +1001,6 @@ defineAgent({
|
|
|
641
1001
|
});
|
|
642
1002
|
},
|
|
643
1003
|
|
|
644
|
-
onUserSpeechFinal({ sessionId, text }) {
|
|
645
|
-
const state = sessions.get(sessionId);
|
|
646
|
-
if (!state || state.demo !== "orbit") {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
speak(sessionId, `You said: ${text}`);
|
|
650
|
-
},
|
|
651
|
-
|
|
652
1004
|
onDataChannelMessage(ctx) {
|
|
653
1005
|
const message = parseShowcaseMessage(ctx.message);
|
|
654
1006
|
if (!message) {
|
|
@@ -657,6 +1009,24 @@ defineAgent({
|
|
|
657
1009
|
void dispatchMessage(ctx.sessionId, message);
|
|
658
1010
|
},
|
|
659
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
|
+
|
|
660
1030
|
onSessionEnd({ sessionId }) {
|
|
661
1031
|
teardownSession(sessionId);
|
|
662
1032
|
agentLog("info", `spatial-showcase session_end ${sessionId}`, sessionId);
|