class LipsyncPlayer {
private client: ConvaiClient;
private isPlaying: boolean = false;
private animationFrameId: number | null = null;
private startTime: number = 0;
constructor(
client: ConvaiClient,
private onFrame: (frame: Float32Array) => void
) {
this.client = client;
// 跟踪机器人开始说话的时间以同步时序
client.on('speakingChange', (isSpeaking) => {
if (isSpeaking) {
this.startTime = performance.now();
}
});
}
start(): void {
if (this.isPlaying) return;
this.isPlaying = true;
this.animate();
}
stop(): void {
if (!this.isPlaying) return;
this.isPlaying = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
private animate = (): void => {
if (!this.isPlaying) return;
const queue = this.client.blendshapeQueue;
if (queue.hasFrames() && queue.isConversationActive()) {
// 计算自机器人开始说话以来经过的时间
const elapsedTime = (performance.now() - this.startTime) / 1000;
// 根据经过时间获取帧(与音频同步)
const result = queue.getFrameAtTime(elapsedTime);
if (result) {
this.onFrame(result.frame);
}
}
this.animationFrameId = requestAnimationFrame(this.animate);
};
}
// 用法
const lipsyncPlayer = new LipsyncPlayer(client, (blendshapes) => {
applyBlendshapesToCharacter(blendshapes, character.morphTargetInfluences);
});
lipsyncPlayer.start();
// 辅助函数:将 blendshape 映射到你的角色的 morph target
function applyBlendshapesToCharacter(frame: Float32Array, influences: number[]) {
// 简单直接映射(前 N 个 blendshape 到前 N 个 morph target)
const maxIndex = Math.min(frame.length, influences.length);
for (let i = 0; i < maxIndex; i++) {
influences[i] = frame[i];
}
// 或者,如果你的角色 morph 排列顺序不同,可使用自定义映射:
// influences[10] = frame[17]; // 将 jawOpen(ARKit 索引 17)映射到你的下巴 morph(索引 10)
// influences[15] = frame[18]; // 将 mouthClose(ARKit 索引 18)映射到你的嘴部 morph(索引 15)
}