Compare commits

..

No commits in common. "1c4dbfc402ab2a1192c616f2eb9f26d34afae33d" and "5e18e7cc8c8ccac729c7606d0223db730e92f734" have entirely different histories.

3 changed files with 13 additions and 100 deletions

View File

@ -630,26 +630,7 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::FeedExternalAudio(const TArray<float>
// Route through relay (clients can't call Server RPCs on NPC actors). // Route through relay (clients can't call Server RPCs on NPC actors).
if (auto* Relay = FindLocalRelayComponent()) if (auto* Relay = FindLocalRelayComponent())
{ {
// Opus-compress mic audio before sending over the network. Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
// 3200 bytes raw PCM → ~200 bytes Opus (~16x reduction).
if (OpusEncoder.IsValid())
{
uint32 CompressedSize = static_cast<uint32>(OpusWorkBuffer.Num());
OpusEncoder->Encode(MicAccumulationBuffer.GetData(),
MicAccumulationBuffer.Num(),
OpusWorkBuffer.GetData(), CompressedSize);
if (CompressedSize > 0)
{
TArray<uint8> Compressed;
Compressed.Append(OpusWorkBuffer.GetData(), CompressedSize);
Relay->ServerRelayMicAudio(GetOwner(), Compressed);
}
}
else
{
// Fallback: raw PCM (no Opus encoder available).
Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
}
} }
else else
{ {
@ -660,33 +641,6 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::FeedExternalAudio(const TArray<float>
} }
} }
// ─────────────────────────────────────────────────────────────────────────────
// Network audio helpers (used by InteractionComponent relay)
// ─────────────────────────────────────────────────────────────────────────────
bool UPS_AI_ConvAgent_ElevenLabsComponent::DecompressMicAudio(
const TArray<uint8>& CompressedData, TArray<uint8>& OutPCM) const
{
if (!OpusDecoder.IsValid() || CompressedData.Num() >= GetMicChunkMinBytes())
{
return false; // Not Opus-compressed or no decoder.
}
const uint32 MaxDecoded = 16000 * 2; // 1 sec of 16kHz 16-bit mono
OutPCM.SetNumUninitialized(MaxDecoded);
uint32 DecodedSize = MaxDecoded;
OpusDecoder->Decode(CompressedData.GetData(), CompressedData.Num(),
OutPCM.GetData(), DecodedSize);
if (DecodedSize == 0) return false;
OutPCM.SetNum(DecodedSize, EAllowShrinking::No);
return true;
}
int32 UPS_AI_ConvAgent_ElevenLabsComponent::GetMicChunkMinBytesPublic() const
{
return GetMicChunkMinBytes();
}
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
// State queries // State queries
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -1359,26 +1313,9 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::OnMicrophoneDataCaptured(const TArray
else else
{ {
// Route through relay (clients can't call Server RPCs on NPC actors). // Route through relay (clients can't call Server RPCs on NPC actors).
// Opus-compress before sending — same logic as FeedExternalAudio.
if (auto* Relay = FindLocalRelayComponent()) if (auto* Relay = FindLocalRelayComponent())
{ {
if (OpusEncoder.IsValid()) Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
{
uint32 CompressedSize = static_cast<uint32>(OpusWorkBuffer.Num());
OpusEncoder->Encode(MicAccumulationBuffer.GetData(),
MicAccumulationBuffer.Num(),
OpusWorkBuffer.GetData(), CompressedSize);
if (CompressedSize > 0)
{
TArray<uint8> Compressed;
Compressed.Append(OpusWorkBuffer.GetData(), CompressedSize);
Relay->ServerRelayMicAudio(GetOwner(), Compressed);
}
}
else
{
Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
}
} }
else else
{ {
@ -1717,16 +1654,14 @@ void UPS_AI_ConvAgent_ElevenLabsComponent::InitOpusCodec()
FVoiceModule& VoiceModule = FVoiceModule::Get(); FVoiceModule& VoiceModule = FVoiceModule::Get();
const ENetRole Role = GetOwnerRole(); const ENetRole Role = GetOwnerRole();
if (Role == ROLE_Authority)
{
OpusEncoder = VoiceModule.CreateVoiceEncoder(
PS_AI_ConvAgent_Audio_ElevenLabs::SampleRate,
PS_AI_ConvAgent_Audio_ElevenLabs::Channels,
EAudioEncodeHint::VoiceEncode_Voice);
}
// Encoder: on Authority it encodes agent audio for multicast to clients.
// On clients it encodes mic audio for relay to server (~16x compression).
OpusEncoder = VoiceModule.CreateVoiceEncoder(
PS_AI_ConvAgent_Audio_ElevenLabs::SampleRate,
PS_AI_ConvAgent_Audio_ElevenLabs::Channels,
EAudioEncodeHint::VoiceEncode_Voice);
// Decoder: on clients it decodes agent audio from multicast.
// On Authority it decodes mic audio arriving via relay from clients.
OpusDecoder = VoiceModule.CreateVoiceDecoder( OpusDecoder = VoiceModule.CreateVoiceDecoder(
PS_AI_ConvAgent_Audio_ElevenLabs::SampleRate, PS_AI_ConvAgent_Audio_ElevenLabs::SampleRate,
PS_AI_ConvAgent_Audio_ElevenLabs::Channels); PS_AI_ConvAgent_Audio_ElevenLabs::Channels);

View File

@ -603,25 +603,13 @@ void UPS_AI_ConvAgent_InteractionComponent::ServerRelayEndConversation_Implement
} }
void UPS_AI_ConvAgent_InteractionComponent::ServerRelayMicAudio_Implementation( void UPS_AI_ConvAgent_InteractionComponent::ServerRelayMicAudio_Implementation(
AActor* AgentActor, const TArray<uint8>& AudioBytes) AActor* AgentActor, const TArray<uint8>& PCMBytes)
{ {
if (!AgentActor) return; if (!AgentActor) return;
auto* Agent = AgentActor->FindComponentByClass<UPS_AI_ConvAgent_ElevenLabsComponent>(); auto* Agent = AgentActor->FindComponentByClass<UPS_AI_ConvAgent_ElevenLabsComponent>();
if (!Agent) return; if (!Agent) return;
// Clients Opus-encode mic audio before sending via relay (~200 bytes Agent->ServerSendMicAudio_Implementation(PCMBytes);
// instead of 3200 bytes per 100ms chunk). Decode back to raw PCM here
// before forwarding to the WebSocket which expects uncompressed int16.
TArray<uint8> DecodedPCM;
if (Agent->DecompressMicAudio(AudioBytes, DecodedPCM))
{
Agent->ServerSendMicAudio_Implementation(DecodedPCM);
}
else
{
// Raw PCM fallback (no Opus or data is already uncompressed).
Agent->ServerSendMicAudio_Implementation(AudioBytes);
}
} }
void UPS_AI_ConvAgent_InteractionComponent::ServerRelaySendText_Implementation( void UPS_AI_ConvAgent_InteractionComponent::ServerRelaySendText_Implementation(

View File

@ -460,16 +460,6 @@ public:
FActorComponentTickFunction* ThisTickFunction) override; FActorComponentTickFunction* ThisTickFunction) override;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override; virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// ── Network audio helpers (used by InteractionComponent relay) ────────
/** Decompress Opus-encoded mic audio back to raw PCM.
* Returns true and fills OutPCM on success; returns false if no Opus
* decoder is available or the data doesn't look compressed. */
bool DecompressMicAudio(const TArray<uint8>& CompressedData, TArray<uint8>& OutPCM) const;
/** Minimum raw PCM bytes expected per mic chunk (used to detect Opus vs raw). */
int32 GetMicChunkMinBytesPublic() const;
private: private:
// ── Network OnRep handlers ─────────────────────────────────────────────── // ── Network OnRep handlers ───────────────────────────────────────────────
UFUNCTION() UFUNCTION()
@ -612,8 +602,8 @@ private:
int32 GetMicChunkMinBytes() const { return MicChunkDurationMs * 32; } int32 GetMicChunkMinBytes() const { return MicChunkDurationMs * 32; }
// ── Opus codec (network audio compression) ─────────────────────────────── // ── Opus codec (network audio compression) ───────────────────────────────
TSharedPtr<IVoiceEncoder> OpusEncoder; // All: server encodes agent audio, clients encode mic audio TSharedPtr<IVoiceEncoder> OpusEncoder; // Server only
TSharedPtr<IVoiceDecoder> OpusDecoder; // All: clients decode agent audio, server decodes mic audio TSharedPtr<IVoiceDecoder> OpusDecoder; // All clients
TArray<uint8> OpusWorkBuffer; // Reusable scratch buffer for encode/decode TArray<uint8> OpusWorkBuffer; // Reusable scratch buffer for encode/decode
void InitOpusCodec(); void InitOpusCodec();