Compare commits

..

2 Commits

Author SHA1 Message Date
0124de8b53 Fix audio pre-buffer bypass on clients due to network sub-chunking
Raw PCM is split into 32KB sub-chunks for network transmission.
On the client, these sub-chunks arrive nearly simultaneously,
triggering the "second chunk arrived" fast-path which cancelled
the pre-buffer after ~10ms instead of the intended 2000ms.

Now the fast-path only applies on Authority (server) where
chunks represent genuine separate TTS batches. Clients always
wait the full pre-buffer duration via TickComponent timer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:19:05 +01:00
c0c1b2cea4 Fix head tracking on remote clients: replicate Pawn instead of PlayerController
PlayerControllers have bOnlyRelevantToOwner=true, so NetConversatingPlayer
was always nullptr on remote clients. Added NetConversatingPawn (APawn*)
which IS replicated to all clients. OnRep_ConversationState now uses
NetConversatingPawn as the posture TargetActor, enabling head/eye
tracking on all clients.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:16:48 +01:00
2 changed files with 38 additions and 21 deletions

View File

@ -225,6 +225,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::StartConversation()
APlayerController* PC = GetWorld()->GetFirstPlayerController();
bNetIsConversing = true;
NetConversatingPlayer = PC;
NetConversatingPawn = PC ? PC->GetPawn() : nullptr;
}
StartConversation_Internal();
}
@ -298,6 +299,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::EndConversation()
// Reset replicated state so other players can talk to this NPC.
bNetIsConversing = false;
NetConversatingPlayer = nullptr;
NetConversatingPawn = nullptr;
}
else
{
@ -608,6 +610,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleDisconnected(int32 StatusCode,
{
bNetIsConversing = false;
NetConversatingPlayer = nullptr;
NetConversatingPawn = nullptr;
}
OnAgentDisconnected.Broadcast(StatusCode, Reason);
@ -1032,20 +1035,26 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::EnqueueAgentAudio(const TArray<uint8>
else if (bPreBuffering)
{
// Second (or later) audio chunk arrived during pre-buffer period.
// We now have both chunks buffered — start playback immediately.
bPreBuffering = false;
if (bDebug)
// On Authority: this means a genuine second TTS chunk arrived from the
// WebSocket, so we have enough data buffered — start playback immediately.
// On Clients: sub-chunks from network splitting arrive nearly simultaneously,
// which would defeat the pre-buffer. Let the timer in TickComponent handle it.
if (GetOwnerRole() == ROLE_Authority)
{
const double NowPb = FPlatformTime::Seconds();
const double BufferedMs = (NowPb - PreBufferStartTime) * 1000.0;
const double Tpb3 = NowPb - SessionStartTime;
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
TEXT("[T+%.2fs] [Turn %d] Pre-buffer: second chunk arrived (%.0fms buffered). Starting playback."),
Tpb3, LastClosedTurnIndex, BufferedMs);
}
if (AudioPlaybackComponent && !AudioPlaybackComponent->IsPlaying())
{
AudioPlaybackComponent->Play();
bPreBuffering = false;
if (bDebug)
{
const double NowPb = FPlatformTime::Seconds();
const double BufferedMs = (NowPb - PreBufferStartTime) * 1000.0;
const double Tpb3 = NowPb - SessionStartTime;
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
TEXT("[T+%.2fs] [Turn %d] Pre-buffer: second chunk arrived (%.0fms buffered). Starting playback."),
Tpb3, LastClosedTurnIndex, BufferedMs);
}
if (AudioPlaybackComponent && !AudioPlaybackComponent->IsPlaying())
{
AudioPlaybackComponent->Play();
}
}
SilentTickCount = 0;
}
@ -1195,6 +1204,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::GetLifetimeReplicatedProps(
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, bNetIsConversing);
DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, NetConversatingPlayer);
DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, NetConversatingPawn);
DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, CurrentEmotion);
DOREPLIFETIME(UPS_AI_ConvAgent_ElevenLabsComponent, CurrentEmotionIntensity);
}
@ -1210,14 +1220,14 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::OnRep_ConversationState()
// on the local pawn, but remote clients never run that code path.
if (UPS_AI_ConvAgent_PostureComponent* Posture = Owner->FindComponentByClass<UPS_AI_ConvAgent_PostureComponent>())
{
if (bNetIsConversing && NetConversatingPlayer)
// Use NetConversatingPawn (replicated to ALL clients) instead of
// NetConversatingPlayer->GetPawn() — PlayerControllers are only
// replicated to their owning client (bOnlyRelevantToOwner=true).
if (bNetIsConversing && NetConversatingPawn)
{
if (APawn* PlayerPawn = NetConversatingPlayer->GetPawn())
{
Posture->TargetActor = PlayerPawn;
Posture->ResetBodyTarget();
Posture->bEnableBodyTracking = true;
}
Posture->TargetActor = NetConversatingPawn;
Posture->ResetBodyTarget();
Posture->bEnableBodyTracking = true;
}
else
{
@ -1271,6 +1281,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerRequestConversation_Implementat
bNetIsConversing = true;
NetConversatingPlayer = RequestingPlayer;
NetConversatingPawn = RequestingPlayer ? RequestingPlayer->GetPawn() : nullptr;
StartConversation_Internal();
}
@ -1289,6 +1300,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerReleaseConversation_Implementat
bNetIsConversing = false;
NetConversatingPlayer = nullptr;
NetConversatingPawn = nullptr;
}
void UPS_AI_ConvAgent_ElevenLabsComponent::ServerSendMicAudio_Implementation(

View File

@ -296,10 +296,15 @@ public:
bool bNetIsConversing = false;
/** The player controller currently in conversation with this NPC (null if free).
* Replicated so each client knows who is speaking (used for posture target, LOD). */
* Only valid on server and owning client (PlayerControllers are not replicated to other clients). */
UPROPERTY(ReplicatedUsing = OnRep_ConversationState, BlueprintReadOnly, Category = "PS AI ConvAgent|Network")
TObjectPtr<APlayerController> NetConversatingPlayer = nullptr;
/** The pawn of the conversating player. Replicated to ALL clients (unlike PlayerController).
* Used by remote clients for posture target (head/eye tracking) and LOD distance checks. */
UPROPERTY(ReplicatedUsing = OnRep_ConversationState, BlueprintReadOnly, Category = "PS AI ConvAgent|Network")
TObjectPtr<APawn> NetConversatingPawn = nullptr;
// ── Network LOD ──────────────────────────────────────────────────────────
/** Distance (cm) beyond which remote clients stop receiving agent audio entirely.