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 fb92f5f..ab04703 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 @@ -1231,27 +1231,49 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleAudioReceived(const TArray(OpusWorkBuffer.Num()); - OpusEncoder->Encode(PCMData.GetData(), PCMData.Num(), - OpusWorkBuffer.GetData(), CompressedSize); + // Opus path: compress then send, in sub-blocks. + // + // A whole ElevenLabs message can be several seconds. Two hard limits + // force us to split it before each Encode()/RPC: + // 1. UE's Opus encoder caps ONE packet at 76 frames (~1.52s) — frames + // beyond that are silently dropped at encode (VoiceCodecOpus.cpp). + // 2. The client decode buffer only holds ~44 frames (~0.88s) before + // the engine decoder aborts with "Decompression buffer too small" + // and drops the packet tail (MulticastReceiveAgentAudio_Implementation). + // 40 frames (25600 bytes, 0.8s) stays safely under BOTH, so nothing is + // truncated on server or client. It also keeps each Unreliable RPC small. + static constexpr int32 BytesPerFrame = + (PS_AI_ConvAgent_Audio_ElevenLabs::SampleRate / 50) + * PS_AI_ConvAgent_Audio_ElevenLabs::Channels + * static_cast(sizeof(int16)); // 640 at 16kHz mono + static constexpr int32 MaxOpusChunkBytes = 40 * BytesPerFrame; // 25600 = 0.8s - if (CompressedSize > 0) + int32 Offset = 0; + while (Offset < PCMData.Num()) { - TArray CompressedData; - CompressedData.Append(OpusWorkBuffer.GetData(), CompressedSize); + const int32 BlockBytes = FMath::Min(MaxOpusChunkBytes, PCMData.Num() - Offset); - if (bNetAudioDebug) + uint32 CompressedSize = static_cast(OpusWorkBuffer.Num()); + OpusEncoder->Encode(PCMData.GetData() + Offset, BlockBytes, + OpusWorkBuffer.GetData(), CompressedSize); + + if (CompressedSize > 0) { - const float Ratio = (CompressedSize > 0) - ? static_cast(PCMData.Num()) / static_cast(CompressedSize) - : 0.0f; - UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log, - TEXT("[NET-SRV] Opus audio chunk: %u bytes sent (raw %d → %.1fx compression)."), - CompressedSize, PCMData.Num(), Ratio); + TArray CompressedData; + CompressedData.Append(OpusWorkBuffer.GetData(), CompressedSize); + + if (bNetAudioDebug) + { + const float Ratio = static_cast(BlockBytes) / static_cast(CompressedSize); + UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log, + TEXT("[NET-SRV] Opus audio chunk: %u bytes sent (raw %d → %.1fx compression)."), + CompressedSize, BlockBytes, Ratio); + } + + MulticastReceiveAgentAudio(CompressedData); } - MulticastReceiveAgentAudio(CompressedData); + Offset += BlockBytes; } } else @@ -2533,7 +2555,12 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::MulticastReceiveAgentAudio_Implementa if (OpusDecoder.IsValid()) { // Opus path: decode compressed audio. - const uint32 MaxDecompressedSize = 16000 * 2; + // Buffer must hold a full packet's decoded PCM. The engine decoder aborts + // with "Decompression buffer too small to decode voice" (dropping the + // packet tail) if this is smaller than the packet needs. The server caps + // packets at 40 frames (25600 bytes); 64KB gives ample headroom even up to + // the engine's 76-frame (~48640 bytes) maximum, so no packet is truncated. + const uint32 MaxDecompressedSize = 64 * 1024; PCMBuffer.SetNumUninitialized(MaxDecompressedSize); uint32 DecompressedSize = MaxDecompressedSize; OpusDecoder->Decode(AudioData.GetData(), AudioData.Num(),