|
|
|
|
@@ -1231,9 +1231,38 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleAudioReceived(const TArray<uint
|
|
|
|
|
|
|
|
|
|
if (OpusEncoder.IsValid())
|
|
|
|
|
{
|
|
|
|
|
// Opus path: compress then send.
|
|
|
|
|
// 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<int32>(sizeof(int16)); // 640 at 16kHz mono
|
|
|
|
|
static constexpr int32 MaxOpusChunkBytes = 40 * BytesPerFrame; // 25600 = 0.8s
|
|
|
|
|
|
|
|
|
|
// Frame-align the message length (Encode drops any sub-frame remainder
|
|
|
|
|
// anyway). A frame-aligned length guarantees every sub-chunk holds >=1
|
|
|
|
|
// whole frame and lets us reliably mark the FINAL sub-chunk (bLast), so
|
|
|
|
|
// the client reassembles one message into a single enqueue (no per-chunk
|
|
|
|
|
// playback re-arm → no pops).
|
|
|
|
|
const int32 UsableBytes = (PCMData.Num() / BytesPerFrame) * BytesPerFrame;
|
|
|
|
|
|
|
|
|
|
int32 Offset = 0;
|
|
|
|
|
while (Offset < UsableBytes)
|
|
|
|
|
{
|
|
|
|
|
const int32 BlockBytes = FMath::Min(MaxOpusChunkBytes, UsableBytes - Offset);
|
|
|
|
|
const bool bLast = (Offset + BlockBytes >= UsableBytes);
|
|
|
|
|
|
|
|
|
|
uint32 CompressedSize = static_cast<uint32>(OpusWorkBuffer.Num());
|
|
|
|
|
OpusEncoder->Encode(PCMData.GetData(), PCMData.Num(),
|
|
|
|
|
OpusEncoder->Encode(PCMData.GetData() + Offset, BlockBytes,
|
|
|
|
|
OpusWorkBuffer.GetData(), CompressedSize);
|
|
|
|
|
|
|
|
|
|
if (CompressedSize > 0)
|
|
|
|
|
@@ -1243,49 +1272,61 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleAudioReceived(const TArray<uint
|
|
|
|
|
|
|
|
|
|
if (bNetAudioDebug)
|
|
|
|
|
{
|
|
|
|
|
const float Ratio = (CompressedSize > 0)
|
|
|
|
|
? static_cast<float>(PCMData.Num()) / static_cast<float>(CompressedSize)
|
|
|
|
|
: 0.0f;
|
|
|
|
|
const float Ratio = static_cast<float>(BlockBytes) / static_cast<float>(CompressedSize);
|
|
|
|
|
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
|
|
|
|
|
TEXT("[NET-SRV] Opus audio chunk: %u bytes sent (raw %d → %.1fx compression)."),
|
|
|
|
|
CompressedSize, PCMData.Num(), Ratio);
|
|
|
|
|
TEXT("[NET-SRV] Opus audio chunk: %u bytes sent (raw %d → %.1fx compression, last=%d)."),
|
|
|
|
|
CompressedSize, BlockBytes, Ratio, bLast ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MulticastReceiveAgentAudio(CompressedData);
|
|
|
|
|
MulticastReceiveAgentAudio(CompressedData, bLast);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Offset += BlockBytes;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Fallback: send raw PCM (no compression). ~32 KB/s at 16kHz 16-bit mono.
|
|
|
|
|
// Only hit if Opus init failed. Chunk small so the Unreliable RPC is
|
|
|
|
|
// NOT auto-promoted to reliable when fragmented: UE re-sends an
|
|
|
|
|
// unreliable bunch reliably once it splits into more than ~8 partial
|
|
|
|
|
// bunches, which would reintroduce the reliable-buffer overflow.
|
|
|
|
|
// ~4 KB ≈ 4 bunches → stays unreliable.
|
|
|
|
|
static bool bWarnedOnce = false;
|
|
|
|
|
if (!bWarnedOnce)
|
|
|
|
|
{
|
|
|
|
|
bWarnedOnce = true;
|
|
|
|
|
// Fallback: Opus init failed → send raw PCM (no compression, ~32 KB/s).
|
|
|
|
|
// This should not happen in the working config — it means [Voice]
|
|
|
|
|
// bEnabled=true is missing in DefaultEngine.ini. Warn on every message
|
|
|
|
|
// so it is loud, not hidden behind a one-shot flag.
|
|
|
|
|
// The RPC is now Reliable, so we must protect the reliable buffer (512
|
|
|
|
|
// bunches): each ~4 KB chunk ≈ ~4 partial bunches. Hard-cap one message
|
|
|
|
|
// at MaxFallbackBytes and drop the tail so a single message can never
|
|
|
|
|
// approach the buffer and trigger a reliable-buffer overflow (the exact
|
|
|
|
|
// cause of the old VR disconnect).
|
|
|
|
|
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
|
|
|
|
TEXT("[NET-SRV] Opus encoder unavailable — sending raw PCM in chunks (no compression)."));
|
|
|
|
|
TEXT("[NET-SRV] Opus encoder unavailable — sending raw PCM (no compression). "
|
|
|
|
|
"Set [Voice] bEnabled=true in DefaultEngine.ini to restore Opus."));
|
|
|
|
|
|
|
|
|
|
static constexpr int32 MaxChunkBytes = 4000;
|
|
|
|
|
static constexpr int32 MaxFallbackBytes = 320000; // ~80 reliable chunks/message, well under the 512-bunch buffer
|
|
|
|
|
|
|
|
|
|
const int32 SendableBytes = FMath::Min(PCMData.Num(), MaxFallbackBytes);
|
|
|
|
|
if (PCMData.Num() > MaxFallbackBytes)
|
|
|
|
|
{
|
|
|
|
|
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
|
|
|
|
TEXT("[NET-SRV] Raw-PCM fallback capped at %d bytes — dropping %d-byte tail "
|
|
|
|
|
"to protect the reliable buffer."),
|
|
|
|
|
MaxFallbackBytes, PCMData.Num() - MaxFallbackBytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static constexpr int32 MaxChunkBytes = 4000; // small enough to stay unreliable (avoid partial-bunch reliable promotion)
|
|
|
|
|
int32 Offset = 0;
|
|
|
|
|
while (Offset < PCMData.Num())
|
|
|
|
|
while (Offset < SendableBytes)
|
|
|
|
|
{
|
|
|
|
|
const int32 ChunkSize = FMath::Min(MaxChunkBytes, PCMData.Num() - Offset);
|
|
|
|
|
const int32 ChunkSize = FMath::Min(MaxChunkBytes, SendableBytes - Offset);
|
|
|
|
|
const bool bLast = (Offset + ChunkSize >= SendableBytes);
|
|
|
|
|
TArray<uint8> Chunk;
|
|
|
|
|
Chunk.Append(PCMData.GetData() + Offset, ChunkSize);
|
|
|
|
|
|
|
|
|
|
if (bNetAudioDebug)
|
|
|
|
|
{
|
|
|
|
|
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
|
|
|
|
TEXT("[NET-SRV] RAW PCM chunk: %d bytes sent (NO compression — Opus disabled)."),
|
|
|
|
|
ChunkSize);
|
|
|
|
|
TEXT("[NET-SRV] RAW PCM chunk: %d bytes sent (NO compression — Opus disabled, last=%d)."),
|
|
|
|
|
ChunkSize, bLast ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MulticastReceiveAgentAudio(Chunk);
|
|
|
|
|
MulticastReceiveAgentAudio(Chunk, bLast);
|
|
|
|
|
Offset += ChunkSize;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1851,6 +1892,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::StopAgentAudio()
|
|
|
|
|
bool bWasSpeaking = false;
|
|
|
|
|
double Now = 0.0;
|
|
|
|
|
bPreBuffering = false; // Clear pre-buffer state on stop.
|
|
|
|
|
ClientReassemblyPCM.Reset(); // Drop any half-received message so it can't leak into the next turn.
|
|
|
|
|
{
|
|
|
|
|
FScopeLock Lock(&AudioQueueLock);
|
|
|
|
|
AudioQueue.Empty();
|
|
|
|
|
@@ -2515,7 +2557,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ClientConversationFailed_Implementati
|
|
|
|
|
// Network: Multicast RPCs
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
void UPS_AI_ConvAgent_ElevenLabsComponent::MulticastReceiveAgentAudio_Implementation(
|
|
|
|
|
const TArray<uint8>& AudioData)
|
|
|
|
|
const TArray<uint8>& AudioData, bool bLastSubChunk)
|
|
|
|
|
{
|
|
|
|
|
// Server already handled playback in HandleAudioReceived.
|
|
|
|
|
if (GetOwnerRole() == ROLE_Authority) return;
|
|
|
|
|
@@ -2533,13 +2575,17 @@ 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(),
|
|
|
|
|
PCMBuffer.GetData(), DecompressedSize);
|
|
|
|
|
if (DecompressedSize == 0) return;
|
|
|
|
|
PCMBuffer.SetNum(DecompressedSize);
|
|
|
|
|
PCMBuffer.SetNum(DecompressedSize); // may be 0 — handled by reassembly below
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
@@ -2547,13 +2593,33 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::MulticastReceiveAgentAudio_Implementa
|
|
|
|
|
PCMBuffer = AudioData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Local playback.
|
|
|
|
|
EnqueueAgentAudio(PCMBuffer);
|
|
|
|
|
// Reassemble one ElevenLabs message from its N sub-chunks, then enqueue ONCE.
|
|
|
|
|
// The server splits a message into sub-chunks (to respect the Opus 76-frame /
|
|
|
|
|
// client decode-buffer limits), sent back-to-back in the same tick. Enqueuing
|
|
|
|
|
// each sub-chunk separately re-ran the playback state machine → a pop at each
|
|
|
|
|
// ~0.8s seam. Accumulating and enqueuing once per message plays it contiguously.
|
|
|
|
|
if (PCMBuffer.Num() > 0)
|
|
|
|
|
{
|
|
|
|
|
ClientReassemblyPCM.Append(PCMBuffer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!bLastSubChunk)
|
|
|
|
|
{
|
|
|
|
|
return; // wait for the remaining sub-chunks of this message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ClientReassemblyPCM.Num() > 0)
|
|
|
|
|
{
|
|
|
|
|
// Local playback (one contiguous block per message).
|
|
|
|
|
EnqueueAgentAudio(ClientReassemblyPCM);
|
|
|
|
|
|
|
|
|
|
// Feed lip-sync (within LOD or speaker).
|
|
|
|
|
if (bIsSpeaker || LipSyncLODDistance <= 0.f || Dist <= LipSyncLODDistance)
|
|
|
|
|
{
|
|
|
|
|
OnAgentAudioData.Broadcast(PCMBuffer);
|
|
|
|
|
OnAgentAudioData.Broadcast(ClientReassemblyPCM);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ClientReassemblyPCM.Reset();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|