Compare commits
2 Commits
5e18e7cc8c
...
1c4dbfc402
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c4dbfc402 | |||
| 86b7d9744e |
@ -630,7 +630,26 @@ 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())
|
||||||
{
|
{
|
||||||
Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
|
// Opus-compress mic audio before sending over the network.
|
||||||
|
// 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
|
||||||
{
|
{
|
||||||
@ -641,6 +660,33 @@ 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
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@ -1313,9 +1359,26 @@ 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())
|
||||||
{
|
{
|
||||||
Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
|
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
|
||||||
|
{
|
||||||
|
Relay->ServerRelayMicAudio(GetOwner(), MicAccumulationBuffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -1654,14 +1717,16 @@ 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);
|
||||||
|
|||||||
@ -603,13 +603,25 @@ 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>& PCMBytes)
|
AActor* AgentActor, const TArray<uint8>& AudioBytes)
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|
||||||
Agent->ServerSendMicAudio_Implementation(PCMBytes);
|
// Clients Opus-encode mic audio before sending via relay (~200 bytes
|
||||||
|
// 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(
|
||||||
|
|||||||
@ -460,6 +460,16 @@ 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()
|
||||||
@ -602,8 +612,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; // Server only
|
TSharedPtr<IVoiceEncoder> OpusEncoder; // All: server encodes agent audio, clients encode mic audio
|
||||||
TSharedPtr<IVoiceDecoder> OpusDecoder; // All clients
|
TSharedPtr<IVoiceDecoder> OpusDecoder; // All: clients decode agent audio, server decodes mic audio
|
||||||
TArray<uint8> OpusWorkBuffer; // Reusable scratch buffer for encode/decode
|
TArray<uint8> OpusWorkBuffer; // Reusable scratch buffer for encode/decode
|
||||||
|
|
||||||
void InitOpusCodec();
|
void InitOpusCodec();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user