Fix network audio: raw PCM fallback when Opus codecs unavailable
UE5 5.5's FVoiceModule returns NULL encoder/decoder, breaking Opus-based audio replication. This adds a transparent fallback: - Server sends raw PCM when OpusEncoder is null (~32KB/s, fine for LAN) - Client accepts raw PCM when OpusDecoder is null - Changed MulticastReceiveAgentAudio from Unreliable to Reliable to handle larger uncompressed payloads without packet loss - Added OnlineSubsystemNull config for future Opus compatibility - Removed premature bAgentSpeaking=true from MulticastAgentStartedSpeaking to fix race condition with audio initialization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5e1c50edf8
commit
6cac56fa06
@ -153,6 +153,12 @@ FontDPI=72
|
|||||||
+EnumRedirects=(OldName="EPS_AI_Agent_Emotion", NewName="EPS_AI_ConvAgent_Emotion")
|
+EnumRedirects=(OldName="EPS_AI_Agent_Emotion", NewName="EPS_AI_ConvAgent_Emotion")
|
||||||
+EnumRedirects=(OldName="EPS_AI_Agent_EmotionIntensity", NewName="EPS_AI_ConvAgent_EmotionIntensity")
|
+EnumRedirects=(OldName="EPS_AI_Agent_EmotionIntensity", NewName="EPS_AI_ConvAgent_EmotionIntensity")
|
||||||
|
|
||||||
|
[OnlineSubsystem]
|
||||||
|
DefaultPlatformService=Null
|
||||||
|
|
||||||
|
[OnlineSubsystemNull]
|
||||||
|
bEnabled=true
|
||||||
|
|
||||||
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
|
[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]
|
||||||
bEnablePlugin=True
|
bEnablePlugin=True
|
||||||
bAllowNetworkConnection=True
|
bAllowNetworkConnection=True
|
||||||
|
|||||||
@ -637,46 +637,35 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::HandleAudioReceived(const TArray<uint
|
|||||||
QueueBefore, (static_cast<float>(QueueBefore) / 16000.0f) * 1000.0f);
|
QueueBefore, (static_cast<float>(QueueBefore) / 16000.0f) * 1000.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Network: Opus-compress and broadcast to all clients before local playback.
|
// Network: broadcast audio to all clients.
|
||||||
if (GetOwnerRole() != ROLE_Authority || !OpusEncoder.IsValid())
|
if (GetOwnerRole() == ROLE_Authority)
|
||||||
{
|
{
|
||||||
static bool bWarnedOnce = false;
|
if (OpusEncoder.IsValid())
|
||||||
if (!bWarnedOnce)
|
|
||||||
{
|
{
|
||||||
bWarnedOnce = true;
|
// Opus path: compress then send.
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
uint32 CompressedSize = static_cast<uint32>(OpusWorkBuffer.Num());
|
||||||
TEXT("[NET-SRV] Cannot multicast audio! Role=%d (need %d=Authority), OpusEncoder=%s, FVoiceModule=%s"),
|
OpusEncoder->Encode(PCMData.GetData(), PCMData.Num(),
|
||||||
static_cast<int32>(GetOwnerRole()), static_cast<int32>(ROLE_Authority),
|
OpusWorkBuffer.GetData(), CompressedSize);
|
||||||
OpusEncoder.IsValid() ? TEXT("VALID") : TEXT("NULL"),
|
|
||||||
FVoiceModule::IsAvailable() ? TEXT("available") : TEXT("UNAVAILABLE"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (GetOwnerRole() == ROLE_Authority && OpusEncoder.IsValid())
|
|
||||||
{
|
|
||||||
uint32 CompressedSize = static_cast<uint32>(OpusWorkBuffer.Num());
|
|
||||||
int32 Remainder = OpusEncoder->Encode(PCMData.GetData(), PCMData.Num(),
|
|
||||||
OpusWorkBuffer.GetData(), CompressedSize);
|
|
||||||
|
|
||||||
if (CompressedSize > 0)
|
if (CompressedSize > 0)
|
||||||
{
|
|
||||||
TArray<uint8> CompressedData;
|
|
||||||
CompressedData.Append(OpusWorkBuffer.GetData(), CompressedSize);
|
|
||||||
|
|
||||||
if (bDebug)
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
|
TArray<uint8> CompressedData;
|
||||||
TEXT("[NET-SRV] Multicasting Opus audio: %d bytes PCM → %d bytes Opus (%.1f:1 ratio)"),
|
CompressedData.Append(OpusWorkBuffer.GetData(), CompressedSize);
|
||||||
PCMData.Num(), CompressedSize,
|
MulticastReceiveAgentAudio(CompressedData);
|
||||||
PCMData.Num() > 0 ? static_cast<float>(PCMData.Num()) / CompressedSize : 0.f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MulticastReceiveAgentAudio(CompressedData);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
// Fallback: send raw PCM (no compression). ~32 KB/s at 16kHz 16-bit mono.
|
||||||
TEXT("[NET-SRV] Opus encode produced 0 bytes from %d bytes PCM — audio not sent."),
|
// Fine for LAN; revisit with proper Opus if internet play is needed.
|
||||||
PCMData.Num());
|
static bool bWarnedOnce = false;
|
||||||
|
if (!bWarnedOnce)
|
||||||
|
{
|
||||||
|
bWarnedOnce = true;
|
||||||
|
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
||||||
|
TEXT("[NET-SRV] Opus encoder unavailable — sending raw PCM (no compression)."));
|
||||||
|
}
|
||||||
|
MulticastReceiveAgentAudio(PCMData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1346,57 +1335,36 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::ClientConversationFailed_Implementati
|
|||||||
// Network: Multicast RPCs
|
// Network: Multicast RPCs
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
void UPS_AI_ConvAgent_ElevenLabsComponent::MulticastReceiveAgentAudio_Implementation(
|
void UPS_AI_ConvAgent_ElevenLabsComponent::MulticastReceiveAgentAudio_Implementation(
|
||||||
const TArray<uint8>& OpusData)
|
const TArray<uint8>& AudioData)
|
||||||
{
|
{
|
||||||
// Server already handled playback in HandleAudioReceived.
|
// Server already handled playback in HandleAudioReceived.
|
||||||
if (GetOwnerRole() == ROLE_Authority) return;
|
if (GetOwnerRole() == ROLE_Authority) return;
|
||||||
|
|
||||||
if (!OpusDecoder.IsValid())
|
|
||||||
{
|
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
|
||||||
TEXT("[NET] MulticastReceiveAgentAudio: OpusDecoder is INVALID — audio dropped. FVoiceModule available: %s"),
|
|
||||||
FVoiceModule::IsAvailable() ? TEXT("YES") : TEXT("NO"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// LOD: skip audio if too far (unless this client is the speaker).
|
// LOD: skip audio if too far (unless this client is the speaker).
|
||||||
const float Dist = GetDistanceToLocalPlayer();
|
const float Dist = GetDistanceToLocalPlayer();
|
||||||
const bool bIsSpeaker = IsLocalPlayerConversating();
|
const bool bIsSpeaker = IsLocalPlayerConversating();
|
||||||
if (!bIsSpeaker && AudioLODCullDistance > 0.f && Dist > AudioLODCullDistance)
|
if (!bIsSpeaker && AudioLODCullDistance > 0.f && Dist > AudioLODCullDistance)
|
||||||
{
|
{
|
||||||
if (bDebug && DebugVerbosity >= 2)
|
|
||||||
{
|
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
|
|
||||||
TEXT("[NET] MulticastReceiveAgentAudio: LOD culled (dist=%.0f > cull=%.0f)"),
|
|
||||||
Dist, AudioLODCullDistance);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode Opus → PCM.
|
|
||||||
const uint32 MaxDecompressedSize = 16000 * 2; // 1 second of 16kHz 16-bit mono
|
|
||||||
TArray<uint8> PCMBuffer;
|
TArray<uint8> PCMBuffer;
|
||||||
PCMBuffer.SetNumUninitialized(MaxDecompressedSize);
|
|
||||||
uint32 DecompressedSize = MaxDecompressedSize;
|
|
||||||
OpusDecoder->Decode(OpusData.GetData(), OpusData.Num(),
|
|
||||||
PCMBuffer.GetData(), DecompressedSize);
|
|
||||||
|
|
||||||
if (DecompressedSize == 0)
|
if (OpusDecoder.IsValid())
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Warning,
|
// Opus path: decode compressed audio.
|
||||||
TEXT("[NET] MulticastReceiveAgentAudio: Opus decode failed (0 bytes output from %d bytes input)"),
|
const uint32 MaxDecompressedSize = 16000 * 2;
|
||||||
OpusData.Num());
|
PCMBuffer.SetNumUninitialized(MaxDecompressedSize);
|
||||||
return;
|
uint32 DecompressedSize = MaxDecompressedSize;
|
||||||
|
OpusDecoder->Decode(AudioData.GetData(), AudioData.Num(),
|
||||||
|
PCMBuffer.GetData(), DecompressedSize);
|
||||||
|
if (DecompressedSize == 0) return;
|
||||||
|
PCMBuffer.SetNum(DecompressedSize);
|
||||||
}
|
}
|
||||||
PCMBuffer.SetNum(DecompressedSize);
|
else
|
||||||
|
|
||||||
if (bDebug)
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_ConvAgent_ElevenLabs, Log,
|
// Fallback: data is already raw PCM (sent uncompressed by server).
|
||||||
TEXT("[NET] MulticastReceiveAgentAudio: decoded %d bytes Opus → %d bytes PCM | bAgentSpeaking=%s | AudioComp playing=%s"),
|
PCMBuffer = AudioData;
|
||||||
OpusData.Num(), DecompressedSize,
|
|
||||||
bAgentSpeaking ? TEXT("true") : TEXT("false"),
|
|
||||||
(AudioPlaybackComponent && AudioPlaybackComponent->IsPlaying()) ? TEXT("true") : TEXT("false"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local playback.
|
// Local playback.
|
||||||
|
|||||||
@ -339,9 +339,9 @@ public:
|
|||||||
UFUNCTION(Server, Reliable)
|
UFUNCTION(Server, Reliable)
|
||||||
void ServerRequestInterrupt();
|
void ServerRequestInterrupt();
|
||||||
|
|
||||||
/** Broadcast Opus-compressed agent audio to all clients. */
|
/** Broadcast agent audio to all clients (Opus-compressed or raw PCM fallback). */
|
||||||
UFUNCTION(NetMulticast, Unreliable)
|
UFUNCTION(NetMulticast, Reliable)
|
||||||
void MulticastReceiveAgentAudio(const TArray<uint8>& OpusData);
|
void MulticastReceiveAgentAudio(const TArray<uint8>& AudioData);
|
||||||
|
|
||||||
/** Notify all clients that the agent started speaking (first audio chunk). */
|
/** Notify all clients that the agent started speaking (first audio chunk). */
|
||||||
UFUNCTION(NetMulticast, Reliable)
|
UFUNCTION(NetMulticast, Reliable)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user