diff --git a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp index 38d2f4c..9fbfe8e 100644 --- a/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp +++ b/Unreal/PS_AI_Agent/Plugins/PS_AI_ConvAgent/Source/PS_AI_ConvAgent/Private/PS_AI_ConvAgent_ElevenLabsComponent.cpp @@ -2369,16 +2369,44 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ServerSendMicAudioFromPlayer( } const double Now = FPlatformTime::Seconds(); - LastSpeakTime.FindOrAdd(SpeakerPawn) = Now; - // If this player IS the active speaker, forward immediately. + // Real voice energy of this chunk (int16 mono PCM). Only ACTUAL speech may + // hold or take the floor — not the silence-tail or ambient that an open mic + // streams continuously. Otherwise the first speaker's continuous stream keeps + // refreshing LastSpeakTime, so no one else can ever switch in and the agent + // stays locked on them (multi-player gaze/turn-taking broken). + const int32 NumSamples = PCMBytes.Num() / (int32)sizeof(int16); + double SumSq = 0.0; + if (NumSamples > 0) + { + const int16* S = reinterpret_cast(PCMBytes.GetData()); + for (int32 i = 0; i < NumSamples; ++i) { const double v = S[i] / 32768.0; SumSq += v * v; } + } + const double Rms = (NumSamples > 0) ? FMath::Sqrt(SumSq / NumSamples) : 0.0; + const bool bVoiced = (Rms >= 0.02); // speech vs ambient/breath; tune if needed. + + // Only real voice asserts holding/taking the floor. + if (bVoiced) + { + LastSpeakTime.FindOrAdd(SpeakerPawn) = Now; + } + + // The active speaker is always forwarded (keeps the silence tail flowing to + // ElevenLabs VAD for end-of-turn detection). if (NetActiveSpeakerPawn == SpeakerPawn) { WebSocketProxy->SendAudioChunk(PCMBytes); return; } - // Speaker switch: only switch if current speaker has been silent + // A different player can only TAKE the floor with actual voice — their + // silence/tail must not steal it from the current speaker. + if (!bVoiced) + { + return; + } + + // Speaker switch: only switch if current speaker has been voice-silent // for at least SpeakerSwitchHysteresis seconds. bool bCanSwitch = true; if (NetActiveSpeakerPawn)