Commit 2a1309bc authored by Danny Pires's avatar Danny Pires 💬

Merge branch 'third-sprint' into 'master'

Voice chat plugin added

See merge request !13
parents e098a689 c6fcad33
......@@ -7,8 +7,13 @@ GameName=VRClassroom
[Voice]
bEnabled=true
[OnlineSubsystem]
bHasVoiceEnabled=true
[/Script/OnlineSubsystemUtils.IpNetDriver]
MaxClientRate=50000
MaxInternetClientRate=50000
[/Script/Engine.Player]
ConfiguredInternetSpeed=500000
ConfiguredLanSpeed=500000
[HTTP]
HttpTimeout=300
......@@ -370,3 +375,7 @@ net.MaxRepArraySize=65535
[/Script/OculusHMD.OculusHMDRuntimeSettings]
bRequiresSystemKeyboard=True
[/Script/Engine.AudioSettings]
MaximumConcurrentStreams=5
VoiPSoundClass=/Game/Campus/Blueprints/SC_CSoundClass.SC_CSoundClass
......@@ -12,6 +12,12 @@ SupportContact=""
CopyrightNotice=Copyright 2020 Testy
ProjectDisplayedTitle=NSLOCTEXT("[/Script/EngineSettings]", "837C4DE649564002BDAE8187B61096E6", "{GameName}")
[/Script/Engine.GameNetworkManager]
TotalNetBandwidth=600000
MaxDynamicBandwidth=80000
MinDynamicBandwidth=4000
[/Script/Engine.GameSession]
bRequiresPushToTalk=false
......
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.2",
"FriendlyName": "Easy Voice Chat",
"Description": "Easy to integrate VOIP using blueprints.",
"Category": "Voice",
"CreatedBy": "313 Studios",
"CreatedByURL": "https://313-studios.com",
"DocsURL": "https://313-studios.com/wp-content/uploads/2019/08/VoiceChatDocumentation.pdf",
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/b204a1777cb34ff88bcc32bd9296c412",
"SupportURL": "support@313-studios.com",
"EngineVersion": "4.25.4",
"CanContainContent": false,
"Installed": true,
"Modules": [
{
"Name": "EasyVoiceChat",
"Type": "Runtime",
"LoadingPhase": "Default",
"WhitelistPlatforms": [
"Win32",
"Win64"
]
}
],
"Plugins": [
{
"Name": "OnlineSubsystemUtils",
"Enabled": true
}
]
}
\ No newline at end of file
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class EasyVoiceChat : ModuleRules
{
public EasyVoiceChat(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"CoreUObject",
"Engine",
"Voice",
"OnlineSubsystemUtils",
"AudioMixer",
"SignalProcessing"
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#include "EasyVoiceChat.h"
#define LOCTEXT_NAMESPACE "FEasyVoiceChatModule"
void FEasyVoiceChatModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}
void FEasyVoiceChatModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FEasyVoiceChatModule, EasyVoiceChat)
\ No newline at end of file
// Copyright 2019 313 Studios. All Rights Reserved.
#include "VoiceFunctionLibrary.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/GameStateBase.h"
#include "GameFramework/PlayerState.h"
#include "Kismet/KismetMathLibrary.h"
TArray<APawn*> UVoiceFunctionLibrary::GetAllPawnsFromState(UObject * WorldContextObject, APawn* CurrentPlayer, float Distance)
{
AGameStateBase* GameState = UGameplayStatics::GetGameState(WorldContextObject);
const bool bUseDistance = Distance > 0.f;
FVector PlayerLocation;
if (CurrentPlayer)
{
PlayerLocation = CurrentPlayer->GetActorLocation();
}
APawn* Pawn;
TArray<APawn*> PawnArray;
if (GameState)
{
for (APlayerState* State : GameState->PlayerArray)
{
Pawn = State->GetPawn();
if(Pawn)
{
if (bUseDistance)
{
// if using distance filtering, only add pawns that are below the user defined distance
if (UKismetMathLibrary::Vector_Distance(Pawn->GetActorLocation(), PlayerLocation) <= Distance)
{
PawnArray.Add(Pawn);
}
}
else
{
PawnArray.Add(Pawn);
}
}
}
}
return PawnArray;
}
// Copyright 2019 313 Studios. All Rights Reserved.
#include "VoipAudioComponent.h"
#include "Net/VoiceConfig.h"
#include "Voice/Public/Voice.h"
UVoipAudioComponent::UVoipAudioComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
}
void UVoipAudioComponent::BeginPlay()
{
Super::BeginPlay();
VoiceDecoder = FVoiceModule::Get().CreateVoiceDecoder();
OpenPacketStream(UVOIPStatics::GetVoiceSampleRate(), UVOIPStatics::GetNumBufferedPackets(), UVOIPStatics::GetBufferingDelay());
ResetBuffer(UVOIPStatics::GetVoiceSampleRate(), UVOIPStatics::GetBufferingDelay());
Initialize(UVOIPStatics::GetVoiceSampleRate());
//Start();
bVoiceStarted = true;
}
void UVoipAudioComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction * ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (IsIdling() && IsActive())
{
// Stop when we're idle to free up for other streaming audio sources
// this tells the synth to stop on the render thread, must use bVoiceGenerating to make sure it has stopped
Stop();
SetActive(false);
UncompressedVoiceData.Empty();
}
}
void UVoipAudioComponent::PlayVoiceData(const TArray<uint8>& CompressedVoiceData)
{
if (!bVoiceStarted)
{
return;
}
if (!IsPlaying() || !IsActive())
{
if (bVoiceGenerating)
{
// if the synth is still generating voice, then don't try to restart voice
// this will give errors, and cause a packet buffer overflow
return;
}
ResetBuffer(UVOIPStatics::GetVoiceSampleRate(), UVOIPStatics::GetBufferingDelay());
Start();
}
const int32 NumCompressedBytes = CompressedVoiceData.Num();
if (VoiceDecoder.IsValid())
{
uint32 BytesWritten = UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel();
UncompressedVoiceData.Empty(UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel());
UncompressedVoiceData.AddUninitialized(UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel());
// Decompress the data
VoiceDecoder->Decode(CompressedVoiceData.GetData(), NumCompressedBytes, UncompressedVoiceData.GetData(), BytesWritten);
// If there is no data, return
if (BytesWritten <= 0)
{
return;
}
// submit the array to the synth component for playback
SubmitPacket((float*)UncompressedVoiceData.GetData(), BytesWritten, UVOIPStatics::GetVoiceSampleRate(), EVoipStreamDataFormat::Int16);
}
}
void UVoipAudioComponent::OnEndGenerate()
{
Super::OnEndGenerate();
bVoiceGenerating = false;
}
void UVoipAudioComponent::OnBeginGenerate()
{
Super::OnBeginGenerate();
bVoiceGenerating = true;
}
// Copyright 2019 313 Studios. All Rights Reserved.
#include "VoipManagerComponent.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/PlayerState.h"
#include "Voice/Public/VoiceModule.h"
#include "Voice/Public/Interfaces/VoiceCodec.h"
#include "Engine/Player.h"
#define MAX_VOICE_REMAINDER_SIZE 1 * 1024
UVoipManagerComponent::UVoipManagerComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
}
// Called every frame
void UVoipManagerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!VoiceCapture.IsValid() || !VoiceEncoder.IsValid())
{
return;
}
// Empty the voice buffers
DecompressedVoiceBuffer.Empty(UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel());
CompressedVoiceBuffer.Empty(UVOIPStatics::GetMaxCompressedVoiceDataSize());
uint32 NewVoiceDataBytes = 0;
EVoiceCaptureState::Type VoiceResult = VoiceCapture->GetCaptureState(NewVoiceDataBytes);
if (!bVoiceActive && NewVoiceDataBytes == 0)
{
// No capture data present, stop processing
return;
}
uint32 TotalVoiceBytes = NewVoiceDataBytes + VoiceRemainderSize;
if (TotalVoiceBytes > UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel())
{
TotalVoiceBytes = UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel();
}
DecompressedVoiceBuffer.AddUninitialized(TotalVoiceBytes);
// If there's still audio left from a previous ReadLocalData call that didn't get output, copy that first into the decompressed voice buffer
if (VoiceRemainderSize > 0)
{
FMemory::Memcpy(DecompressedVoiceBuffer.GetData(), VoiceRemainder.GetData(), VoiceRemainderSize);
}
// Get new uncompressed data
uint8* RemainingDecompressedBufferPtr = DecompressedVoiceBuffer.GetData() + VoiceRemainderSize;
uint32 ByteWritten = 0;
VoiceResult = VoiceCapture->GetVoiceData(DecompressedVoiceBuffer.GetData() + VoiceRemainderSize, NewVoiceDataBytes, ByteWritten);
TotalVoiceBytes = ByteWritten + VoiceRemainderSize;
if ((VoiceResult == EVoiceCaptureState::Ok || VoiceResult == EVoiceCaptureState::NoData) && TotalVoiceBytes > 0)
{
// Prepare the encoded buffer (e.g. opus)
CompressedBytesAvailable = UVOIPStatics::GetMaxCompressedVoiceDataSize();
CompressedVoiceBuffer.AddUninitialized(UVOIPStatics::GetMaxCompressedVoiceDataSize());
check(((uint32)CompressedVoiceBuffer.Num()) <= UVOIPStatics::GetMaxCompressedVoiceDataSize());
// Run the uncompressed audio through the opus decoder, note that it may not encode all data, which results in some remaining data
VoiceRemainderSize = VoiceEncoder->Encode(DecompressedVoiceBuffer.GetData(), TotalVoiceBytes, CompressedVoiceBuffer.GetData(), CompressedBytesAvailable);
// Save off any unencoded remainder
if (VoiceRemainderSize > 0)
{
if (VoiceRemainderSize > MAX_VOICE_REMAINDER_SIZE)
{
VoiceRemainderSize = MAX_VOICE_REMAINDER_SIZE;
}
VoiceRemainder.AddUninitialized(MAX_VOICE_REMAINDER_SIZE);
FMemory::Memcpy(VoiceRemainder.GetData(), DecompressedVoiceBuffer.GetData() + (TotalVoiceBytes - VoiceRemainderSize), VoiceRemainderSize);
}
}
if (VoiceResult != EVoiceCaptureState::Ok || TotalVoiceBytes == 0)
{
if (bVoiceActive)
{
// get the current time in seconds
const double CurTime = FPlatformTime::Seconds();
// Work out the time since this player last talked
double TimeSince = CurTime - LastSeen;
if (TimeSince >= StopTalkingThreshold)
{
// Notify blueprints that we've stopped talking
OnPlayerStopTalking();
PlayerStopTalking.Broadcast();
bVoiceActive = false;
}
}
}
// Pass the data to blueprints if there is voice data
if (CompressedBytesAvailable > 0)
{
// update the last seen time, used for the talking threshold
LastSeen = FPlatformTime::Seconds();
if (!bVoiceActive)
{
OnPlayerStartTalking();
PlayerTalking.Broadcast();
bVoiceActive = true;
}
// copy the encoded values into a new array, which only has encoded bytes in
// This prevents copying the entire array, which has lots of unused space and will just saturate the actor channel
OutCompressedVoiceBuffer.Empty(CompressedBytesAvailable);
OutCompressedVoiceBuffer.AddUninitialized(CompressedBytesAvailable);
FMemory::Memcpy(OutCompressedVoiceBuffer.GetData(), CompressedVoiceBuffer.GetData(), CompressedBytesAvailable);
const float MicLevel = VoiceCapture.Get()->GetCurrentAmplitude();
VoiceGenerated.Broadcast(OutCompressedVoiceBuffer, MicLevel);
OnVoiceGeneratedBP(OutCompressedVoiceBuffer, MicLevel);
}
}
bool UVoipManagerComponent::InitVoice(AController* Controller)
{
if (Controller)
{
FVoiceModule& VoiceModule = FVoiceModule::Get();
APlayerController* PlayerController = Cast<APlayerController>(Controller);
if (!PlayerController)
{
return false;
}
if (Controller->GetNetOwningPlayer())
{
Controller->GetNetOwningPlayer()->ConsoleCommand("voice.playback.ShouldResync 0");
if (bAutoSetConsoleVariables)
{
Controller->GetNetOwningPlayer()->ConsoleCommand("voice.SilenceDetectionThreshold " + FString::SanitizeFloat(SilenceDetectionThreshold));
Controller->GetNetOwningPlayer()->ConsoleCommand("voice.MicNoiseGateThreshold " + FString::SanitizeFloat(NoiseGateThreshold));
Controller->GetNetOwningPlayer()->ConsoleCommand("voice.JitterBufferDelay " + FString::SanitizeFloat(VoiceBufferDelay));
}
}
// Only create voice capture on local clients
if (VoiceModule.IsVoiceEnabled() && Controller->IsLocalController())
{
VoiceCapture = FVoiceModule::Get().CreateVoiceCapture(FString(), UVOIPStatics::GetVoiceSampleRate(), UVOIPStatics::GetVoiceNumChannels());
VoiceEncoder = FVoiceModule::Get().CreateVoiceEncoder();
VoiceDecoder = FVoiceModule::Get().CreateVoiceDecoder();
bool bSuccess = VoiceCapture.IsValid() && VoiceEncoder.IsValid();
if (bSuccess)
{
VoiceCapture->Start();
CompressedVoiceBuffer.Empty(UVOIPStatics::GetMaxCompressedVoiceDataSize());
DecompressedVoiceBuffer.Empty(UVOIPStatics::GetMaxUncompressedVoiceDataSizePerChannel());
return true;
}
}
}
return false;
}
// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
class FEasyVoiceChatModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
// Copyright 2019 313 Studios. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "VoiceFunctionLibrary.generated.h"
UCLASS()
class EASYVOICECHAT_API UVoiceFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
/* Get player pawns from the game state. Optionally filter by a distance from the current player */
UFUNCTION(BlueprintPure, Category = "Voice", meta = (WorldContext = "WorldContextObject"))
static TArray<APawn*> GetAllPawnsFromState(UObject* WorldContextObject, APawn* CurrentPlayer, float Distance = 0.f);
};
// Copyright 2019 313 Studios. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "OnlineSubsystemUtils/Public/VoipListenerSynthComponent.h"
#include "VoipAudioComponent.generated.h"
/**
Decodes and plays compressed voice data passed over the network
*/
UCLASS(Blueprintable, ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class EASYVOICECHAT_API UVoipAudioComponent : public UVoipListenerSynthComponent
{
GENERATED_BODY()
public:
UVoipAudioComponent();
virtual void BeginPlay() override;
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/* Plays compressed voice data */
UFUNCTION(BlueprintCallable, Category = "Voip Audio")
void PlayVoiceData(const TArray<uint8> &CompressedVoiceData);
protected:
virtual void OnEndGenerate() override;
virtual void OnBeginGenerate() override;
private:
/* Voice buffer used by this audio component */
TArray<uint8> UncompressedVoiceData;
/* Voice decoder interface */
TSharedPtr<class IVoiceDecoder> VoiceDecoder;
/* Component is active when the audio component is registered */
bool bVoiceStarted = false;
/* Used to check if the audio is being generated on the audio render thread */
bool bVoiceGenerating = false;
};
// Copyright 2019 313 Studios. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "VoipManagerComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FVoiceGenerated, const TArray<uint8>&, VoiceData, const float, MicLevel);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPlayerStartTalking);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FPlayerStopTalking);
class AController;
/*
Generates voice data and sends it to blueprints for processing
*/
UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class EASYVOICECHAT_API UVoipManagerComponent : public UActorComponent
{
GENERATED_BODY()
public:
UVoipManagerComponent();
// Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
/* Call this to start the voice capture. Ensures voice only starts on a local player machine
@return true if capture started successfully
*/
UFUNCTION(BlueprintCallable, Category = "Voip Manager")
bool InitVoice(AController* Controller);
/* Called when voice data is generated, passes an array of compressed voice data to blueprints. Use on inherited components */
UFUNCTION(BlueprintImplementableEvent, Category = "Voip Manager")
void OnVoiceGeneratedBP(const TArray<uint8> &VoiceBuffer, const float MicLevel);
/* Called on the client when the player starts talking */
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category = "Voip Manager")
void OnPlayerStartTalking();
/* Called on the client when the player stops talking */
UFUNCTION(BlueprintImplementableEvent, BlueprintCosmetic, Category = "Voip Manager")
void OnPlayerStopTalking();
/* Delegate called when voice is generated */
UPROPERTY(BlueprintAssignable, Category = "VOIP")
FVoiceGenerated VoiceGenerated;
/* Called when the player starts talking */
UPROPERTY(BlueprintAssignable, Category = "VOIP")
FPlayerStartTalking PlayerTalking;
/* Called when the player stops talking */
UPROPERTY(BlueprintAssignable, Category = "VOIP")
FPlayerStopTalking PlayerStopTalking;
private:
/* Capture interfaces */
TSharedPtr<class IVoiceCapture> VoiceCapture;
TSharedPtr<class IVoiceEncoder> VoiceEncoder;
TSharedPtr<class IVoiceDecoder> VoiceDecoder;
/* Voice buffers */
TArray<uint8> DecompressedVoiceBuffer;
TArray<uint8> CompressedVoiceBuffer;
/* Internally used buffer which is adjusted to the size of the encoded, and is passed to blueprints */
TArray<uint8> OutCompressedVoiceBuffer;
/* Used internally for recieving voice data */
TArray<uint8> DecodedVoiceBuffer;
/* Remainder of voice data carried over if the encode buffer has leftovers */
TArray<uint8> VoiceRemainder;
/* The size of the remaining voice buffer */
uint32 VoiceRemainderSize;
/* The size of the compressed voice buffer */
uint32 CompressedBytesAvailable;
/* The time in seconds this voip was last transmitted */
float LastSeen = 0.0f;
/* The threshold in which Stop Talking event will be called after transmission to compensate for buffering */
UPROPERTY(EditDefaultsOnly, Category="VOIP")
float StopTalkingThreshold = 1.0f;
/* Whether to automatically set the below console variables. Disable if you want to change these variables globally, instead of per component */
UPROPERTY(EditDefaultsOnly, Category = "VOIP")
bool bAutoSetConsoleVariables = true;
/* Transmits silence when below this threshold. When set to the same value as Noise Gate Threshold it has no effect */
UPROPERTY(EditDefaultsOnly, Category = "VOIP", meta = (ClampMin = 0, ClampMax = 1))
float SilenceDetectionThreshold = 0.01f;
/* Stops transmitting audio when it falls below this value. Decrease if the voice cuts in and out */
UPROPERTY(EditDefaultsOnly, Category = "VOIP", meta = (ClampMin = 0, ClampMax = 1))
float NoiseGateThreshold = 0.01f;
/* The amount of time that the voice playback components have to buffer the audio being played back
Decrease if you want to reduce latency
Increase if you experience underruns (audio stuttering or cutting out prematurely) */
UPROPERTY(EditDefaultsOnly, Category = "VOIP")
float VoiceBufferDelay = 0.5f;
/* Set to true whenever the player is talking */
bool bVoiceActive;
};
......@@ -49,6 +49,11 @@
{
"Name": "OculusAvatar",
"Enabled": true
},
{
"Name": "EasyVoiceChat",
"Enabled": true,
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/b204a1777cb34ff88bcc32bd9296c412"
}
],
"TargetPlatforms": [
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment