newly added updated FMODStudio plugin for migration
This commit is contained in:
@ -0,0 +1,20 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
using UnrealBuildTool;
|
||||
using System.IO;
|
||||
|
||||
public struct FMODAudioLink
|
||||
{
|
||||
public static void Apply(UnrealBuildTool.Rules.FMODStudio FMODModule, ReadOnlyTargetRules Target)
|
||||
{
|
||||
FMODModule.AddModule("FMODAudioLink");
|
||||
FMODModule.PrivateDependencyModuleNames.AddRange(
|
||||
new string[]
|
||||
{
|
||||
"AudioLinkCore",
|
||||
"AudioLinkEngine",
|
||||
"SignalProcessing"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IAudioLink.h"
|
||||
#include "AudioDevice.h"
|
||||
#include "IBufferedAudioOutput.h"
|
||||
#include "FMODAudioLinkInputClient.h"
|
||||
|
||||
//* AudioLink Instance, a container holding shared pointers for lifetime management. */
|
||||
struct FFMODAudioLink : IAudioLink
|
||||
{
|
||||
FSharedBufferedOutputPtr ProducerSP;
|
||||
|
||||
FSharedFMODAudioLinkInputClientPtr ConsumerSP;
|
||||
|
||||
FAudioDevice* AudioDevice;
|
||||
|
||||
FFMODAudioLink(const FSharedBufferedOutputPtr& InProducerSP, const FSharedFMODAudioLinkInputClientPtr& InConsumerSP, FAudioDevice* InAudioDevice = nullptr)
|
||||
: ProducerSP(InProducerSP)
|
||||
, ConsumerSP(InConsumerSP)
|
||||
, AudioDevice(InAudioDevice)
|
||||
{}
|
||||
|
||||
virtual ~FFMODAudioLink() override
|
||||
{
|
||||
if (ConsumerSP.IsValid())
|
||||
{
|
||||
ConsumerSP->Stop();
|
||||
}
|
||||
if (AudioDevice && ProducerSP.IsValid())
|
||||
{
|
||||
ProducerSP->Stop(AudioDevice);
|
||||
}
|
||||
}
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkComponent.h"
|
||||
#include "FMODAudioLinkFactory.h"
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
|
||||
void UFMODAudioLinkComponent::CreateLink()
|
||||
{
|
||||
if (!Settings)
|
||||
{
|
||||
Settings = GetMutableDefault<UFMODAudioLinkSettings>();
|
||||
}
|
||||
|
||||
IAudioLinkFactory* Factory = IAudioLinkFactory::FindFactory(FFMODAudioLinkFactory::GetFactoryNameStatic());
|
||||
if (Factory)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("UFMODAudioLinkComponent::CreateLink."));
|
||||
IAudioLinkFactory::FAudioLinkSourceCreateArgs CreateArgs;
|
||||
CreateArgs.OwningComponent = this;
|
||||
CreateArgs.AudioComponent = AudioComponent;
|
||||
CreateArgs.Settings = Settings;
|
||||
LinkInstance = Factory->CreateSourceAudioLink(CreateArgs);
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::CreateAudioComponent()
|
||||
{
|
||||
if (!AudioComponent)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("UFMODAudioLinkComponent::CreateAudioComponent."));
|
||||
AudioComponent = NewObject<UAudioComponent>(this);
|
||||
|
||||
if (!AudioComponent->GetAttachParent() && !AudioComponent->IsAttachedTo(this))
|
||||
{
|
||||
AActor* Owner = GetOwner();
|
||||
if (!Owner || !Owner->GetWorld())
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
AudioComponent->RegisterComponentWithWorld(World);
|
||||
AudioComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform);
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioComponent->SetupAttachment(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioComponent->AttachToComponent(this, FAttachmentTransformRules::KeepRelativeTransform);
|
||||
AudioComponent->RegisterComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (AudioComponent)
|
||||
{
|
||||
AudioComponent->bAutoActivate = false;
|
||||
AudioComponent->bStopWhenOwnerDestroyed = true;
|
||||
AudioComponent->bShouldRemainActiveIfDropped = true;
|
||||
AudioComponent->Mobility = EComponentMobility::Movable;
|
||||
|
||||
#if WITH_EDITORONLY_DATA
|
||||
AudioComponent->bVisualizeComponent = false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::OnRegister()
|
||||
{
|
||||
Super::OnRegister();
|
||||
|
||||
CreateAudioComponent();
|
||||
|
||||
if (ensure(AudioComponent))
|
||||
{
|
||||
check(LinkInstance == nullptr);
|
||||
CreateLink();
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::OnUnregister()
|
||||
{
|
||||
LinkInstance.Reset();
|
||||
AudioComponent = nullptr;
|
||||
|
||||
Super::OnUnregister();
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::SetLinkSound(USoundBase* InSound)
|
||||
{
|
||||
Sound = InSound;
|
||||
|
||||
if (AudioComponent)
|
||||
{
|
||||
AudioComponent->SetSound(InSound);
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::PlayLink(float StartTime)
|
||||
{
|
||||
if (AudioComponent)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("UFMODAudioLinkComponent::PlayLink."));
|
||||
AudioComponent->SetSound(Sound);
|
||||
AudioComponent->Play(StartTime);
|
||||
|
||||
SetActiveFlag(AudioComponent->IsActive());
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkComponent::StopLink()
|
||||
{
|
||||
if (IsActive())
|
||||
{
|
||||
if (AudioComponent)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("UFMODAudioLinkComponent::StopLink."));
|
||||
AudioComponent->Stop();
|
||||
}
|
||||
|
||||
SetActiveFlag(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool UFMODAudioLinkComponent::IsLinkPlaying() const
|
||||
{
|
||||
return AudioComponent && AudioComponent->IsPlaying();
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkFactory.h"
|
||||
#include "FMODAudioLinkSynchronizer.h"
|
||||
#include "FMODAudioLinkSourcePushed.h"
|
||||
#include "FMODAudioLinkSettings.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
#include "FMODAudioLinkComponent.h"
|
||||
#include "FMODStudioModule.h"
|
||||
|
||||
#include "Async/Async.h"
|
||||
#include "Components/AudioComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Sound/SoundSubmix.h"
|
||||
#include "Templates/SharedPointer.h"
|
||||
#include "AudioDevice.h"
|
||||
|
||||
bool FFMODAudioLinkFactory::bHasSubmix = false;
|
||||
|
||||
FName FFMODAudioLinkFactory::GetFactoryNameStatic()
|
||||
{
|
||||
static const FName FactoryName(TEXT("FMOD"));
|
||||
return FactoryName;
|
||||
}
|
||||
|
||||
FName FFMODAudioLinkFactory::GetFactoryName() const
|
||||
{
|
||||
return GetFactoryNameStatic();
|
||||
}
|
||||
|
||||
TSubclassOf<UAudioLinkSettingsAbstract> FFMODAudioLinkFactory::GetSettingsClass() const
|
||||
{
|
||||
return UFMODAudioLinkSettings::StaticClass();
|
||||
}
|
||||
|
||||
TUniquePtr<IAudioLink> FFMODAudioLinkFactory::CreateSubmixAudioLink(const FAudioLinkSubmixCreateArgs& InArgs)
|
||||
{
|
||||
if (!IFMODStudioModule::IsAvailable())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: No FMODStudio module."));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!InArgs.Settings.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: Invalid FMODAudioLinkSettings."));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!InArgs.Submix.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: Invalid Submix."));
|
||||
return {};
|
||||
}
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: Creating AudioLink %s for Submix %s."), *InArgs.Settings->GetName(), *InArgs.Submix->GetName());
|
||||
bHasSubmix = true;
|
||||
|
||||
// Downcast to settings proxy
|
||||
const FSharedFMODAudioLinkSettingsProxyPtr FMODSettingsSP = InArgs.Settings->GetCastProxy<FFMODAudioLinkSettingsProxy>();
|
||||
|
||||
// Make buffer listener first, which is our producer.
|
||||
IAudioLinkFactory::FSubmixBufferListenerCreateParams SubmixListenerCreateArgs;
|
||||
SubmixListenerCreateArgs.SizeOfBufferInFrames = FMODSettingsSP->GetReceivingBufferSizeInFrames();
|
||||
SubmixListenerCreateArgs.bShouldZeroBuffer = FMODSettingsSP->ShouldClearBufferOnReceipt();
|
||||
FSharedBufferedOutputPtr ProducerSP = CreateSubmixBufferListener(SubmixListenerCreateArgs);
|
||||
TWeakPtr<IBufferedAudioOutput> ProducerWeak(ProducerSP);
|
||||
|
||||
// Create consumer.
|
||||
FSharedFMODAudioLinkInputClientPtr ConsumerSP = MakeShared<FFMODAudioLinkInputClient, ESPMode::ThreadSafe>(
|
||||
ProducerSP, InArgs.Settings->GetProxy(), InArgs.Submix->GetFName());
|
||||
TWeakPtr<FFMODAudioLinkInputClient> ConsumerWeak(ConsumerSP);
|
||||
|
||||
// Setup a delegate to establish the link when we know the format.
|
||||
ProducerSP->SetFormatKnownDelegate(
|
||||
IBufferedAudioOutput::FOnFormatKnown::CreateLambda(
|
||||
[ProducerWeak, ConsumerWeak, FMODSettingsSP](const IBufferedAudioOutput::FBufferFormat& InFormat)
|
||||
{
|
||||
// Unreal uses samples for 'Channels x samples' and frames for 'samples'
|
||||
int32 BufferSizeInChannelSamples = FMODSettingsSP->GetReceivingBufferSizeInFrames() * InFormat.NumChannels;
|
||||
int32 ReserveSizeInChannelSamples = (float)BufferSizeInChannelSamples * FMODSettingsSP->GetProducerConsumerBufferRatio();
|
||||
int32 SilenceToAddToFirstBuffer = FMath::Min((float)BufferSizeInChannelSamples * FMODSettingsSP->GetInitialSilenceFillRatio(), ReserveSizeInChannelSamples);
|
||||
|
||||
// Set circular buffer ahead of first buffer.
|
||||
if (auto ProducerSP = ProducerWeak.Pin())
|
||||
{
|
||||
ProducerSP->Reserve(ReserveSizeInChannelSamples, SilenceToAddToFirstBuffer);
|
||||
}
|
||||
|
||||
|
||||
AsyncTask(ENamedThreads::GameThread, [ConsumerWeak]()
|
||||
{
|
||||
if (FSharedFMODAudioLinkInputClientPtr ConsumerSP = ConsumerWeak.Pin())
|
||||
{
|
||||
// Stop ahead of starting to play. This might not be necessary for submixes, but in case we get a format change.
|
||||
// As our link can remain open, stop anything playing on a format change.
|
||||
// This won't do anything if we're already stopped.
|
||||
ConsumerSP->Stop();
|
||||
|
||||
// Start the FMOD input object.
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: Start consumer."));
|
||||
ConsumerSP->Start();
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
// Start producer.
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSubmixAudioLink: Start producer."));
|
||||
ProducerSP->Start(InArgs.Device);
|
||||
|
||||
// Build a link, which owns both the consumer and producer.
|
||||
return MakeUnique<FFMODAudioLink>(ProducerSP, ConsumerSP, InArgs.Device);
|
||||
}
|
||||
|
||||
TUniquePtr<IAudioLink> FFMODAudioLinkFactory::CreateSourceAudioLink(const FAudioLinkSourceCreateArgs& InArgs)
|
||||
{
|
||||
if (!IFMODStudioModule::IsAvailable())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: No FMODStudio module."));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!InArgs.Settings.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Invalid FMODAudioLinkSettings."));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!InArgs.OwningComponent.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Invalid Owning Component."));
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!InArgs.AudioComponent.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Invalid Audio Component."));
|
||||
return {};
|
||||
}
|
||||
|
||||
const UWorld* World = InArgs.OwningComponent->GetWorld();
|
||||
if (UNLIKELY(!IsValid(World)))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Invalid World in Owning Component."));
|
||||
return {};
|
||||
}
|
||||
|
||||
const FAudioDeviceHandle Handle = World->GetAudioDevice();
|
||||
|
||||
// Downcast to settings proxy.
|
||||
const FSharedFMODAudioLinkSettingsProxyPtr FMODSettingsSP = InArgs.Settings->GetCastProxy<FFMODAudioLinkSettingsProxy>();
|
||||
|
||||
// Make buffer listener first, which is our producer.
|
||||
FSourceBufferListenerCreateParams SourceBufferCreateArgs;
|
||||
SourceBufferCreateArgs.SizeOfBufferInFrames = FMODSettingsSP->GetReceivingBufferSizeInFrames();
|
||||
SourceBufferCreateArgs.bShouldZeroBuffer = true;
|
||||
SourceBufferCreateArgs.OwningComponent = InArgs.OwningComponent;
|
||||
SourceBufferCreateArgs.AudioComponent = InArgs.AudioComponent;
|
||||
FSharedBufferedOutputPtr ProducerSP = CreateSourceBufferListener(SourceBufferCreateArgs);
|
||||
|
||||
static const FName UnknownOwner(TEXT("Unknown"));
|
||||
FName OwnerName = InArgs.OwningComponent.IsValid() ? InArgs.OwningComponent->GetFName() : UnknownOwner;
|
||||
TWeakPtr<IBufferedAudioOutput> ProducerWeak(ProducerSP);
|
||||
|
||||
// Create consumer.
|
||||
FSharedFMODAudioLinkInputClientPtr ConsumerSP = MakeShared<FFMODAudioLinkInputClient, ESPMode::ThreadSafe>(ProducerSP, FMODSettingsSP, OwnerName);
|
||||
TWeakPtr<FFMODAudioLinkInputClient> ConsumerWeak(ConsumerSP);
|
||||
|
||||
ProducerSP->SetFormatKnownDelegate(
|
||||
IBufferedAudioOutput::FOnFormatKnown::CreateLambda(
|
||||
[ProducerWeak, ConsumerWeak, FMODSettingsSP, WeakThis = InArgs.OwningComponent](const IBufferedAudioOutput::FBufferFormat& InFormat)
|
||||
{
|
||||
// Unreal uses samples for 'Channels x samples' and frames for 'samples'
|
||||
int32 BufferSizeInChannelSamples = FMODSettingsSP->GetReceivingBufferSizeInFrames() * InFormat.NumChannels;
|
||||
int32 ReserveSizeInChannelSamples = (float)BufferSizeInChannelSamples * FMODSettingsSP->GetProducerConsumerBufferRatio();
|
||||
int32 SilenceToAddToFirstBuffer = FMath::Min((float)BufferSizeInChannelSamples * FMODSettingsSP->GetInitialSilenceFillRatio(), ReserveSizeInChannelSamples);
|
||||
|
||||
|
||||
// Set circular buffer ahead of first buffer.
|
||||
if (auto ProducerSP = ProducerWeak.Pin())
|
||||
{
|
||||
ProducerSP->Reserve(ReserveSizeInChannelSamples, SilenceToAddToFirstBuffer);
|
||||
}
|
||||
|
||||
AsyncTask(ENamedThreads::GameThread, [ConsumerWeak, WeakThis]()
|
||||
{
|
||||
if (FSharedFMODAudioLinkInputClientPtr ConsumerSP = ConsumerWeak.Pin())
|
||||
{
|
||||
if (WeakThis.IsValid())
|
||||
{
|
||||
// Stop ahead of starting to play. This might not be necessary for submixes, but in case we get a format change.
|
||||
// As our link can remain open, stop anything playing on a format change.
|
||||
// This won't do anything if we're already stopped.
|
||||
ConsumerSP->Stop();
|
||||
|
||||
// Start the FMOD input object.
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Start consumer."));
|
||||
ConsumerSP->Start(Cast<UFMODAudioLinkComponent>(WeakThis.Get()));
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
ProducerSP->SetBufferStreamEndDelegate(
|
||||
IBufferedAudioOutput::FOnBufferStreamEnd::CreateLambda(
|
||||
[ConsumerWeak](const IBufferedAudioOutput::FBufferStreamEnd&)
|
||||
{
|
||||
if (FSharedFMODAudioLinkInputClientPtr ConsumerSP = ConsumerWeak.Pin())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Stop consumer."));
|
||||
ConsumerSP->Stop();
|
||||
}
|
||||
}));
|
||||
|
||||
// Tell the Producer to Start receiving buffers from Sources.
|
||||
// Pass a Lambda to do the some work when we know the Format, which starts FMOD up.
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSourceAudioLink: Start producer."));
|
||||
ProducerSP->Start(Handle ? Handle.GetAudioDevice() : nullptr);
|
||||
|
||||
// Make the link.
|
||||
return MakeUnique<FFMODAudioLink>(ProducerSP, ConsumerSP);
|
||||
}
|
||||
|
||||
IAudioLinkFactory::FAudioLinkSourcePushedSharedPtr FFMODAudioLinkFactory::CreateSourcePushedAudioLink(const FAudioLinkSourcePushedCreateArgs& InArgs)
|
||||
{
|
||||
if (IFMODStudioModule::IsAvailable())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSourcePushedAudioLink: Create AudioLink SourcePushed."));
|
||||
return MakeShared<FFMODAudioLinkSourcePushed, ESPMode::ThreadSafe>(InArgs,this);
|
||||
}
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkFactory::CreateSourcePushedAudioLink: IFMODStudioModule not available."));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IAudioLinkFactory::FAudioLinkSynchronizerSharedPtr FFMODAudioLinkFactory::CreateSynchronizerAudioLink()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSynchronizerAudioLink: Create AudioLink Synchronizer."));
|
||||
auto SynchronizerSP = MakeShared<FFMODAudioLinkSynchronizer, ESPMode::ThreadSafe>();
|
||||
return SynchronizerSP;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FMODAudioLink.h"
|
||||
#include "IAudioLinkFactory.h"
|
||||
|
||||
class FFMODAudioLinkFactory : public IAudioLinkFactory
|
||||
{
|
||||
public:
|
||||
FFMODAudioLinkFactory() = default;
|
||||
virtual ~FFMODAudioLinkFactory() override = default;
|
||||
|
||||
static FName GetFactoryNameStatic();
|
||||
static bool bHasSubmix;
|
||||
|
||||
protected:
|
||||
/** Begin IAudioLinkFactory */
|
||||
FName GetFactoryName() const override;
|
||||
TSubclassOf<UAudioLinkSettingsAbstract> GetSettingsClass() const override;
|
||||
TUniquePtr<IAudioLink> CreateSubmixAudioLink(const FAudioLinkSubmixCreateArgs&) override;
|
||||
TUniquePtr<IAudioLink> CreateSourceAudioLink(const FAudioLinkSourceCreateArgs&) override;
|
||||
FAudioLinkSourcePushedSharedPtr CreateSourcePushedAudioLink(const FAudioLinkSourcePushedCreateArgs&) override;
|
||||
FAudioLinkSynchronizerSharedPtr CreateSynchronizerAudioLink() override;
|
||||
/** End IAudioLinkFactory */
|
||||
};
|
@ -0,0 +1,314 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkInputClient.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
#include "FMODAudioLinkSettings.h"
|
||||
#include "FMODAudioLinkFactory.h"
|
||||
#include "FMODAudioLinkComponent.h"
|
||||
#include "FMODEvent.h"
|
||||
|
||||
#include "FMODStudioModule.h"
|
||||
#include "FMODBlueprintStatics.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
#include "Async/Async.h"
|
||||
#include "Templates/SharedPointer.h"
|
||||
|
||||
class InputClientRef
|
||||
{
|
||||
public:
|
||||
TSharedRef<FFMODAudioLinkInputClient> InputClient;
|
||||
|
||||
InputClientRef(TSharedRef<FFMODAudioLinkInputClient> InputSP)
|
||||
: InputClient(InputSP)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
FMOD::Studio::System* GetStudioSystem()
|
||||
{
|
||||
if (IFMODStudioModule::IsAvailable())
|
||||
{
|
||||
auto* StudioSystem = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
|
||||
if (!StudioSystem)
|
||||
{
|
||||
StudioSystem = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Auditioning);
|
||||
}
|
||||
return StudioSystem;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::Register(const FName& NameOfProducingSource)
|
||||
{
|
||||
const auto Name = NameOfProducingSource.GetPlainNameString();
|
||||
|
||||
if (UNLIKELY(!Settings.IsValid()))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Warning, TEXT("FFMODAudioLinkInputClient::Register: FMODAudioLinkSettings are not valid."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (UNLIKELY(!GetStudioSystem()))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Warning, TEXT("FFMODAudioLinkInputClient::Register: Unable to get FMOD Studio System."));
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncTask(ENamedThreads::GameThread, []
|
||||
{
|
||||
const auto AudioDeviceManager = FAudioDeviceManager::Get();
|
||||
if (UNLIKELY(!AudioDeviceManager))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Warning, TEXT("FFMODAudioLinkInputClient::Register: No AudioDeviceManager at registration."));
|
||||
return;
|
||||
}
|
||||
const auto AudioDevice = AudioDeviceManager->GetActiveAudioDevice();
|
||||
if (UNLIKELY(!AudioDevice))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Warning, TEXT("FFMODAudioLinkInputClient::Register: No active AudioDevice at registration."));
|
||||
return;
|
||||
}
|
||||
UE_CLOG(UNLIKELY(AudioDevice->GetMaxChannels() == 0), LogFMODAudioLink, Warning,
|
||||
TEXT("FMODAudioLink: The current AudioDevice %d has 0 MaxChannels. Consider setting AudioMaxChannels to a sensible value in the Engine config file's TargetSettings for your platform."),
|
||||
AudioDevice->DeviceID);
|
||||
|
||||
UE_CLOG(!FFMODAudioLinkFactory::bHasSubmix,
|
||||
LogFMODAudioLink, Warning, TEXT("FMODAudioLink: No initial submix got routed to AudioLink. Consider creating custom versions of global submixes in Project Settings Audio, and Enable Audio Link in their advanced settings."));
|
||||
});
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::Unregister()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkInputClient::Unregister."));
|
||||
}
|
||||
|
||||
FFMODAudioLinkInputClient::FFMODAudioLinkInputClient(const FSharedBufferedOutputPtr& ToConsumeFrom, const UAudioLinkSettingsAbstract::FSharedSettingsProxyPtr& Settings, FName NameOfProducingSource)
|
||||
: WeakProducer(ToConsumeFrom)
|
||||
, Settings(Settings)
|
||||
, ProducerName(NameOfProducingSource)
|
||||
{
|
||||
check(Settings.IsValid());
|
||||
Register(NameOfProducingSource);
|
||||
UnrealFormat = {};
|
||||
}
|
||||
|
||||
FFMODAudioLinkInputClient::~FFMODAudioLinkInputClient()
|
||||
{
|
||||
Unregister();
|
||||
}
|
||||
|
||||
FMOD_RESULT F_CALLBACK pcmreadcallback(FMOD_SOUND* inSound, void* data, unsigned int datalen)
|
||||
{
|
||||
FMOD::Sound* sound = (FMOD::Sound*)inSound;
|
||||
FFMODAudioLinkInputClient* ConsumerSP;
|
||||
sound->getUserData((void**)&ConsumerSP);
|
||||
|
||||
ConsumerSP->GetSamples(data, datalen);
|
||||
|
||||
return FMOD_OK;
|
||||
}
|
||||
|
||||
FMOD_RESULT F_CALLBACK SoundCallback(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE* event, void* parameters)
|
||||
{
|
||||
FMOD_RESULT result = FMOD_OK;
|
||||
FMOD::Studio::EventInstance* eventInstance = (FMOD::Studio::EventInstance*)event;
|
||||
|
||||
if (type == FMOD_STUDIO_EVENT_CALLBACK_CREATE_PROGRAMMER_SOUND)
|
||||
{
|
||||
InputClientRef* ClientRef;
|
||||
result = eventInstance->getUserData((void**)&ClientRef);
|
||||
|
||||
FFMODAudioLinkInputClient* ConsumerPtr = &ClientRef->InputClient.Get();
|
||||
auto formatInfo = ConsumerPtr->GetFormat();
|
||||
|
||||
FMOD::System* CoreSystem = nullptr;
|
||||
GetStudioSystem()->getCoreSystem(&CoreSystem);
|
||||
|
||||
// Create sound info
|
||||
FMOD_CREATESOUNDEXINFO exinfo;
|
||||
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); /* Required. */
|
||||
exinfo.numchannels = formatInfo->NumChannels; /* Number of channels in the sound. */
|
||||
exinfo.defaultfrequency = formatInfo->NumSamplesPerSec; /* Default playback rate of sound. */
|
||||
exinfo.decodebuffersize = formatInfo->NumSamplesPerBlock / exinfo.numchannels; /* Chunk size of stream update in samples. Should match the FMOD System. */
|
||||
exinfo.length = exinfo.defaultfrequency * exinfo.numchannels * sizeof(signed short) * 5; /* Length of PCM data in bytes of whole song (for Sound::getLength) */
|
||||
exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT; /* Data format of sound. */
|
||||
exinfo.pcmreadcallback = pcmreadcallback; /* User callback for reading. */
|
||||
exinfo.userdata = ConsumerPtr;
|
||||
|
||||
FMOD::Sound* sound = NULL;
|
||||
FString sourceName = ConsumerPtr->GetProducerName().ToString();
|
||||
result = CoreSystem->createSound(TCHAR_TO_ANSI(*sourceName), FMOD_OPENUSER | FMOD_CREATESTREAM, &exinfo, &sound);
|
||||
|
||||
// Pass the sound to FMOD
|
||||
FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES* props = (FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES*)parameters;
|
||||
props->sound = (FMOD_SOUND*)sound;
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("Sound Created: %s , Consumer = %p."), *sourceName, ConsumerPtr);
|
||||
}
|
||||
else if (type == FMOD_STUDIO_EVENT_CALLBACK_DESTROY_PROGRAMMER_SOUND)
|
||||
{
|
||||
// Obtain the sound
|
||||
FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES* props = (FMOD_STUDIO_PROGRAMMER_SOUND_PROPERTIES*)parameters;
|
||||
FMOD::Sound* sound = (FMOD::Sound*)props->sound;
|
||||
|
||||
// Release the sound
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("Sound Release: %p."), sound);
|
||||
result = sound->release();
|
||||
}
|
||||
else if (type == FMOD_STUDIO_EVENT_CALLBACK_DESTROYED)
|
||||
{
|
||||
InputClientRef* ClientRef = nullptr;
|
||||
result = eventInstance->getUserData((void**)&ClientRef);
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("Event Destroyed: ClientRef = %p."), ClientRef);
|
||||
if (ClientRef)
|
||||
{
|
||||
delete ClientRef;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::Start(USceneComponent* InComponent)
|
||||
{
|
||||
Stop();
|
||||
check(!IsLoadedHandle.IsValid());
|
||||
|
||||
FFMODAudioLinkSettingsProxy* FMODSettings = static_cast<FFMODAudioLinkSettingsProxy*>(Settings.Get());
|
||||
const auto LinkEvent = FMODSettings->GetLinkEvent();
|
||||
|
||||
auto SelfSP = AsShared();
|
||||
auto PlayLambda = [SelfSP, LinkEvent, InComponent]()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkInputClient::Start: SelSP = %p, LinkEvent = %s, InComponent = %p."), &SelfSP, LinkEvent.Get(), &InComponent);
|
||||
|
||||
FMOD::Studio::EventDescription* EventDesc = IFMODStudioModule::Get().GetEventDescription(LinkEvent.Get());
|
||||
if (EventDesc != nullptr)
|
||||
{
|
||||
FMOD::Studio::EventInstance* EventInst = NULL;
|
||||
EventDesc->createInstance(&EventInst);
|
||||
SelfSP->EventInstance = EventInst;
|
||||
if (EventInst != nullptr)
|
||||
{
|
||||
FTransform EventTransform = InComponent ? InComponent->GetComponentTransform() : FTransform();
|
||||
FMOD_3D_ATTRIBUTES EventAttr = { { 0 } };
|
||||
FMODUtils::Assign(EventAttr, EventTransform);
|
||||
EventInst->set3DAttributes(&EventAttr);
|
||||
|
||||
EventInst->setCallback(SoundCallback, FMOD_STUDIO_EVENT_CALLBACK_CREATE_PROGRAMMER_SOUND | FMOD_STUDIO_EVENT_CALLBACK_DESTROY_PROGRAMMER_SOUND | FMOD_STUDIO_EVENT_CALLBACK_DESTROYED);
|
||||
|
||||
InputClientRef* callbackMemory = new InputClientRef(SelfSP);
|
||||
|
||||
EventInst->setUserData(callbackMemory);
|
||||
EventInst->start();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
FMODSettings->IsEventDataLoaded() ? PlayLambda() : FMODSettings->RegisterCallback(PlayLambda, IsLoadedHandle);
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::Stop()
|
||||
{
|
||||
if (EventInstance->isValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkInputClient::Stop: Stopping EventInstance."));
|
||||
EventInstance->stop(FMOD_STUDIO_STOP_ALLOWFADEOUT);
|
||||
EventInstance->release();
|
||||
}
|
||||
|
||||
if (IsLoadedHandle.IsValid())
|
||||
{
|
||||
FFMODAudioLinkSettingsProxy* FMODSettings = static_cast<FFMODAudioLinkSettingsProxy*>(Settings.Get());
|
||||
check(FMODSettings);
|
||||
|
||||
FMODSettings->UnRegisterCallback(IsLoadedHandle);
|
||||
IsLoadedHandle.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::UpdateWorldState(const FWorldState& InParams)
|
||||
{
|
||||
if (EventInstance->isValid())
|
||||
{
|
||||
const FTransform& T = InParams.WorldTransform;
|
||||
FMOD_3D_ATTRIBUTES attr = { { 0 } };
|
||||
FMODUtils::Assign(attr, T);
|
||||
|
||||
// TODO: velocity
|
||||
|
||||
EventInstance->set3DAttributes(&attr);
|
||||
}
|
||||
}
|
||||
|
||||
bool FFMODAudioLinkInputClient::GetSamples(void* data, unsigned int dataLenBytes)
|
||||
{
|
||||
FSharedBufferedOutputPtr StrongBufferProducer{ WeakProducer.Pin() };
|
||||
if (!StrongBufferProducer.IsValid())
|
||||
{
|
||||
// return false, to indicate no more data.
|
||||
FMemory::Memzero(data, dataLenBytes);
|
||||
return false;
|
||||
}
|
||||
|
||||
float* dataBuffer = (float*)data;
|
||||
|
||||
int32 FramesWritten = 0;
|
||||
|
||||
int32 dataLenFrames = dataLenBytes / (sizeof(float));
|
||||
|
||||
bool bMoreDataRemaining = StrongBufferProducer->PopBuffer(dataBuffer, dataLenFrames, FramesWritten);
|
||||
|
||||
// Zero any buffer space that we didn't output to.
|
||||
int32 FramesThatNeedZeroing = dataLenFrames - FramesWritten;
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkInputClient::GetSamples: (post-pop), SamplesPopped=%d, SamplesNeeded=%d, ZeroFrames=%d, This=0x%p"),
|
||||
FramesWritten, dataLenFrames, FramesThatNeedZeroing, this);
|
||||
|
||||
if (FramesThatNeedZeroing > 0)
|
||||
{
|
||||
FMemory::Memset(&dataBuffer[FramesWritten], 0, FramesThatNeedZeroing);
|
||||
NumStarvedBuffersInARow++;
|
||||
|
||||
static const int32 NumStatedBuffersBeforeStop = 5;
|
||||
if (NumStarvedBuffersInARow > NumStatedBuffersBeforeStop)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FMODAudioLinkInputClient::GetSamples: Stopping Starving input object, Needed=%d, Red=%d, StarvedCount=%d, This=0x%p"),
|
||||
dataLenFrames, FramesWritten, NumStarvedBuffersInARow, this);
|
||||
|
||||
// Terminate.
|
||||
bMoreDataRemaining = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
NumStarvedBuffersInARow = 0;
|
||||
}
|
||||
|
||||
return bMoreDataRemaining;
|
||||
}
|
||||
|
||||
IBufferedAudioOutput::FBufferFormat* FFMODAudioLinkInputClient::GetFormat()
|
||||
{
|
||||
// Ensure we're still listening to a sub mix that exists.
|
||||
FSharedBufferedOutputPtr StrongPtr{ WeakProducer.Pin() };
|
||||
if (!StrongPtr.IsValid())
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FMODAudioLinkInputClient::GetFormat: FSharedBufferedOutputPtr not valid."));
|
||||
}
|
||||
else
|
||||
{
|
||||
ensure(StrongPtr->GetFormat(UnrealFormat));
|
||||
}
|
||||
|
||||
return &UnrealFormat;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkInputClient::SetFormat(const IBufferedAudioOutput::FBufferFormat *AudioFormat)
|
||||
{
|
||||
UnrealFormat.NumChannels = AudioFormat->NumChannels;
|
||||
UnrealFormat.NumSamplesPerBlock = AudioFormat->NumSamplesPerBlock;
|
||||
UnrealFormat.NumSamplesPerSec = AudioFormat->NumSamplesPerSec;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IAudioLink.h"
|
||||
#include "IAudioLinkFactory.h"
|
||||
#include "Templates/SharedPointer.h"
|
||||
#include "IBufferedAudioOutput.h"
|
||||
#include "DSP/BufferVectorOperations.h"
|
||||
|
||||
#include "fmod_studio.hpp"
|
||||
|
||||
class UFMODAudioLinkComponent;
|
||||
class FFMODAudioLinkInputClient;
|
||||
|
||||
class FFMODAudioLinkInputClient : public TSharedFromThis<FFMODAudioLinkInputClient, ESPMode::ThreadSafe>
|
||||
{
|
||||
public:
|
||||
FFMODAudioLinkInputClient(const FSharedBufferedOutputPtr& InToConsumeFrom, const UAudioLinkSettingsAbstract::FSharedSettingsProxyPtr& InSettings, FName InNameOfProducingSource={});
|
||||
virtual ~FFMODAudioLinkInputClient();
|
||||
|
||||
/// Used by all Audio Link sources.
|
||||
/// Optional component parameter that is used with the FMODAudioLinkComponent.
|
||||
void Start(USceneComponent* InComponent = nullptr);
|
||||
void Stop();
|
||||
|
||||
struct FWorldState
|
||||
{
|
||||
FTransform WorldTransform;
|
||||
};
|
||||
// Called from Consumer thread at game tick rate.
|
||||
void UpdateWorldState(const FWorldState&);
|
||||
// Called from FMOD thread.
|
||||
bool GetSamples(void* data, unsigned int datalen);
|
||||
|
||||
IBufferedAudioOutput::FBufferFormat* GetFormat();
|
||||
|
||||
/// Used Sound "Sources" instead of FormatKnownDelegate to store the format at the time of starting.
|
||||
void SetFormat(const IBufferedAudioOutput::FBufferFormat *AudioFormat);
|
||||
|
||||
FName GetProducerName() const { return ProducerName; }
|
||||
FMOD::Studio::EventInstance* EventInstance;
|
||||
|
||||
private:
|
||||
void Register(const FName& NameOfProducingSource);
|
||||
void Unregister();
|
||||
|
||||
FWeakBufferedOutputPtr WeakProducer;
|
||||
UAudioLinkSettingsAbstract::FSharedSettingsProxyPtr Settings;
|
||||
|
||||
IBufferedAudioOutput::FBufferFormat UnrealFormat;
|
||||
|
||||
FName ProducerName;
|
||||
int32 NumStarvedBuffersInARow = 0;
|
||||
FDelegateHandle IsLoadedHandle;
|
||||
};
|
||||
|
||||
using FSharedFMODAudioLinkInputClientPtr = TSharedPtr<FFMODAudioLinkInputClient, ESPMode::ThreadSafe>;
|
@ -0,0 +1,5 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkLog.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogFMODAudioLink);
|
@ -0,0 +1,150 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkSettings.h"
|
||||
#include "FMODAudioLinkFactory.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
#include "FMODStudioModule.h"
|
||||
#include "FMODEvent.h"
|
||||
#include "fmod_studio.hpp"
|
||||
#include "FMODSettings.h"
|
||||
|
||||
#include "Engine/StreamableManager.h"
|
||||
#include "Engine/AssetManager.h"
|
||||
#include "Async/Async.h"
|
||||
|
||||
|
||||
FFMODAudioLinkSettingsProxy::FFMODAudioLinkSettingsProxy(const UFMODAudioLinkSettings& InSettings)
|
||||
{
|
||||
Update(InSettings);
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSettingsProxy::Update(const UFMODAudioLinkSettings& InSettings)
|
||||
{
|
||||
InSettings.RequestLoadLinkEvent();
|
||||
ReceivingBufferSizeInFrames = InSettings.GetReceivingBufferSizeInFrames();
|
||||
ProducerToConsumerBufferRatio = InSettings.ProducerToConsumerBufferRatio;
|
||||
InitialSilenceFillRatio = InSettings.InitialSilenceFillRatio;
|
||||
FMODLinkEvent = InSettings.LinkEvent;
|
||||
}
|
||||
|
||||
bool FFMODAudioLinkSettingsProxy::IsEventDataLoaded() const
|
||||
{
|
||||
return bIsEventDataLoaded;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSettingsProxy::RegisterCallback(const TFunction<void()>& InCallback, FDelegateHandle& OutHandle)
|
||||
{
|
||||
FScopeLock Lock(&CS);
|
||||
OutHandle = OnEventLoadedDelegate.Add(FSimpleMulticastDelegate::FDelegate::CreateLambda(InCallback));
|
||||
}
|
||||
|
||||
bool FFMODAudioLinkSettingsProxy::UnRegisterCallback(const FDelegateHandle& InDelegate)
|
||||
{
|
||||
FScopeLock Lock(&CS);
|
||||
return OnEventLoadedDelegate.Remove(InDelegate);
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSettingsProxy::NotifyEventDataLoaded()
|
||||
{
|
||||
FScopeLock Lock(&CS);
|
||||
bIsEventDataLoaded = true;
|
||||
OnEventLoadedDelegate.Broadcast();
|
||||
}
|
||||
|
||||
#if WITH_EDITOR
|
||||
void FFMODAudioLinkSettingsProxy::RefreshFromSettings(UAudioLinkSettingsAbstract* InSettings, FPropertyChangedEvent& InPropertyChangedEvent)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkSettingsProxy::RefreshFromSettings."));
|
||||
Update(*CastChecked<UFMODAudioLinkSettings>(InSettings));
|
||||
}
|
||||
#endif //WITH_EDITOR
|
||||
|
||||
|
||||
void UFMODAudioLinkSettings::PostLoad()
|
||||
{
|
||||
RequestLoadLinkEvent();
|
||||
Super::PostLoad();
|
||||
}
|
||||
|
||||
void UFMODAudioLinkSettings::RequestLoadLinkEvent() const
|
||||
{
|
||||
if (bLoadRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
bLoadRequested = true;
|
||||
const UFMODSettings& Settings = *GetDefault<UFMODSettings>();
|
||||
if (Settings.bFMODAudioLinkEnabled)
|
||||
{
|
||||
AsyncTask(ENamedThreads::GameThread, [WeakThis = MakeWeakObjectPtr(const_cast<UFMODAudioLinkSettings*>(this))]()
|
||||
{
|
||||
if (WeakThis.IsValid())
|
||||
{
|
||||
FStreamableManager& StreamableManager = UAssetManager::GetStreamableManager();
|
||||
FStreamableDelegate Delegate = FStreamableDelegate::CreateUObject(WeakThis.Get(), &UFMODAudioLinkSettings::OnLoadCompleteCallback);
|
||||
WeakThis->LoadingHandle = StreamableManager.RequestAsyncLoad(WeakThis->LinkEvent.ToSoftObjectPath(), Delegate, FStreamableManager::AsyncLoadHighPriority,
|
||||
/* Managed active handle */ true);
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkSettings::RequestLoadLinkEvent: Async Loading %s."), *WeakThis->LinkEvent.ToSoftObjectPath().ToString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void UFMODAudioLinkSettings::OnLoadCompleteCallback()
|
||||
{
|
||||
TArray<UObject*> LoadedAssets;
|
||||
LoadingHandle->GetLoadedAssets(LoadedAssets);
|
||||
if (LoadedAssets.Num() > 0 && IsValid(LoadedAssets[0]))
|
||||
{
|
||||
LinkEventResolved = CastChecked<UFMODEvent>(LoadedAssets[0]);
|
||||
if (IsValid(LinkEventResolved))
|
||||
{
|
||||
LinkEventResolved->AddToRoot();
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkSettings::OnLoadCompleteCallback: Unable to resolve Link Event."));
|
||||
}
|
||||
}
|
||||
|
||||
GetCastProxy<FFMODAudioLinkSettingsProxy>()->NotifyEventDataLoaded();
|
||||
LoadingHandle.Reset();
|
||||
}
|
||||
|
||||
void UFMODAudioLinkSettings::FinishDestroy()
|
||||
{
|
||||
if (IsValid(LinkEventResolved))
|
||||
{
|
||||
LinkEventResolved->RemoveFromRoot();
|
||||
}
|
||||
Super::FinishDestroy();
|
||||
}
|
||||
|
||||
int32 UFMODAudioLinkSettings::GetReceivingBufferSizeInFrames() const
|
||||
{
|
||||
if (IFMODStudioModule::IsAvailable())
|
||||
{
|
||||
FMOD::System* CoreSystem;
|
||||
FMOD::Studio::System* StudioSystem = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Max);
|
||||
if (StudioSystem)
|
||||
{
|
||||
StudioSystem->getCoreSystem(&CoreSystem);
|
||||
if (CoreSystem)
|
||||
{
|
||||
unsigned int bufferLength = 0;
|
||||
CoreSystem->getDSPBufferSize(&bufferLength, 0);
|
||||
|
||||
return bufferLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const int32 SensibleDefaultSize = 512;
|
||||
UE_LOG(LogFMODAudioLink, Warning, TEXT("FMODAudioLinkSettings: Failed to get FMOD settings for buffer size, defaulting to '%d'"), SensibleDefaultSize);
|
||||
return SensibleDefaultSize;
|
||||
}
|
||||
|
||||
FName UFMODAudioLinkSettings::GetFactoryName() const
|
||||
{
|
||||
return FFMODAudioLinkFactory::GetFactoryNameStatic();
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkSourcePushed.h"
|
||||
#include "FMODAudioLinkSettings.h"
|
||||
#include "FMODAudioLinkInputClient.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
|
||||
#include "FMODEvent.h"
|
||||
|
||||
FFMODAudioLinkSourcePushed::FFMODAudioLinkSourcePushed(const IAudioLinkFactory::FAudioLinkSourcePushedCreateArgs& InArgs, IAudioLinkFactory* InFactory)
|
||||
: CreateArgs(InArgs)
|
||||
{
|
||||
const FFMODAudioLinkSettingsProxy* FMODSettingsSP = static_cast<FFMODAudioLinkSettingsProxy*>(InArgs.Settings.Get());
|
||||
|
||||
IAudioLinkFactory::FPushedBufferListenerCreateParams Params;
|
||||
Params.SizeOfBufferInFrames = InArgs.NumFramesPerBuffer;
|
||||
Params.bShouldZeroBuffer = FMODSettingsSP->ShouldClearBufferOnReceipt();
|
||||
|
||||
ProducerSP = InFactory->CreatePushableBufferListener(Params);
|
||||
ConsumerSP = MakeShared<FFMODAudioLinkInputClient, ESPMode::ThreadSafe>(ProducerSP, InArgs.Settings, InArgs.OwnerName);
|
||||
|
||||
// Unreal uses samples for 'Channels x samples' and frames for 'samples'
|
||||
int32 BufferSizeInChannelSamples = FMODSettingsSP->GetReceivingBufferSizeInFrames() * InArgs.NumChannels;
|
||||
int32 ReserveSizeInChannelSamples = (float)BufferSizeInChannelSamples * FMODSettingsSP->GetProducerConsumerBufferRatio();
|
||||
int32 SilenceToAddToFirstBuffer = FMath::Min((float)BufferSizeInChannelSamples * FMODSettingsSP->GetInitialSilenceFillRatio(), ReserveSizeInChannelSamples);
|
||||
|
||||
// Set circular buffer ahead of first buffer.
|
||||
ProducerSP->Reserve(ReserveSizeInChannelSamples, SilenceToAddToFirstBuffer);
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose,
|
||||
TEXT("FFMODAudioLinkSourcePushed::Ctor() Name=%s, Producer=0x%p, Consumer=0x%p, p2c%%=%2.2f, PlayEvent=%s, TotalFramesForSource=%d, This=0x%p"),
|
||||
*InArgs.OwnerName.GetPlainNameString(), ProducerSP.Get(),
|
||||
ConsumerSP.Get(), FMODSettingsSP->GetProducerConsumerBufferRatio(), *FMODSettingsSP->GetLinkEvent()->GetName(),
|
||||
CreateArgs.TotalNumFramesInSource, this);
|
||||
}
|
||||
|
||||
FFMODAudioLinkSourcePushed::~FFMODAudioLinkSourcePushed()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose,
|
||||
TEXT("FFMODAudioLinkSourcePushed::Dtor() Name=%s, Producer=0x%p, Consumer=0x%p, RecievedFrames=%d/%d, This=0x%p"),
|
||||
*CreateArgs.OwnerName.GetPlainNameString(), ProducerSP.Get(), ConsumerSP.Get(), NumFramesReceivedSoFar,
|
||||
CreateArgs.TotalNumFramesInSource,this);
|
||||
|
||||
if (ConsumerSP.IsValid())
|
||||
{
|
||||
ConsumerSP->Stop();
|
||||
}
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSourcePushed::OnNewBuffer(const FOnNewBufferParams& InArgs)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, VeryVerbose,
|
||||
TEXT("FFMODAudioLinkSourcePushed::OnNewBuffer() Name=%s, Producer=0x%p, Consumer=0x%p, SourceID=%d, RecievedFrames=%d/%d, This=0x%p"),
|
||||
*CreateArgs.OwnerName.GetPlainNameString(), ProducerSP.Get(), ConsumerSP.Get(), SourceId, NumFramesReceivedSoFar,
|
||||
CreateArgs.TotalNumFramesInSource, this);
|
||||
|
||||
NumFramesReceivedSoFar += CreateArgs.NumFramesPerBuffer;
|
||||
|
||||
if (SourceId == INDEX_NONE)
|
||||
{
|
||||
IBufferedAudioOutput::FBufferFormat AudioFormat = {};
|
||||
AudioFormat.NumChannels = CreateArgs.NumChannels;
|
||||
AudioFormat.NumSamplesPerBlock = CreateArgs.NumFramesPerBuffer;
|
||||
AudioFormat.NumSamplesPerSec = CreateArgs.SampleRate;
|
||||
ConsumerSP->SetFormat(&AudioFormat);
|
||||
|
||||
SourceId = InArgs.SourceId;
|
||||
ProducerSP->Start(nullptr);
|
||||
ConsumerSP->Start();
|
||||
}
|
||||
check(SourceId == InArgs.SourceId);
|
||||
|
||||
IPushableAudioOutput* Pushable = ProducerSP->GetPushableInterface();
|
||||
if (ensure(Pushable))
|
||||
{
|
||||
IPushableAudioOutput::FOnNewBufferParams Params;
|
||||
Params.AudioData = InArgs.Buffer.GetData();
|
||||
Params.NumSamples = InArgs.Buffer.Num();
|
||||
Params.Id = InArgs.SourceId;
|
||||
Params.NumChannels = CreateArgs.NumChannels;
|
||||
Params.SampleRate = CreateArgs.SampleRate;
|
||||
Pushable->PushNewBuffer(Params);
|
||||
}
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSourcePushed::OnSourceDone(const int32 InSourceId)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose,
|
||||
TEXT("FFMODAudioLinkSourcePushed::OnSourceDone() Name=%s, Producer=0x%p, Consumer=0x%p, RecievedFrames=%d/%d, This=0x%p"),
|
||||
*CreateArgs.OwnerName.GetPlainNameString(), ProducerSP.Get(), ConsumerSP.Get(), NumFramesReceivedSoFar,
|
||||
CreateArgs.TotalNumFramesInSource, this);
|
||||
|
||||
check(SourceId == InSourceId);
|
||||
IPushableAudioOutput* Pushable = ProducerSP->GetPushableInterface();
|
||||
if (ensure(Pushable))
|
||||
{
|
||||
Pushable->LastBuffer(SourceId);
|
||||
}
|
||||
SourceId = INDEX_NONE;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSourcePushed::OnSourceReleased(const int32 InSourceId)
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose,
|
||||
TEXT("FFMODAudioLinkSourcePushed::OnSourceReleased() Name=%s, Producer=0x%p, Consumer=0x%p, RecievedFrames=%d/%d, This=0x%p"),
|
||||
*CreateArgs.OwnerName.GetPlainNameString(), ProducerSP.Get(), ConsumerSP.Get(), NumFramesReceivedSoFar,
|
||||
CreateArgs.TotalNumFramesInSource,this);
|
||||
}
|
||||
|
||||
// Called by the AudioThread, not the AudioRenderThread
|
||||
void FFMODAudioLinkSourcePushed::OnUpdateWorldState(const FOnUpdateWorldStateParams& InParams)
|
||||
{
|
||||
FFMODAudioLinkInputClient::FWorldState UpdateParams;
|
||||
UpdateParams.WorldTransform = InParams.WorldTransform;
|
||||
ConsumerSP->UpdateWorldState(UpdateParams);
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
#include "FMODAudioLinkInputClient.h"
|
||||
|
||||
#include "IAudioLink.h"
|
||||
#include "IBufferedAudioOutput.h"
|
||||
|
||||
struct FFMODAudioLinkSourcePushed : IAudioLinkSourcePushed
|
||||
{
|
||||
int32 SourceId = INDEX_NONE;
|
||||
int32 NumFramesReceivedSoFar = INDEX_NONE;
|
||||
FSharedBufferedOutputPtr ProducerSP;
|
||||
FSharedFMODAudioLinkInputClientPtr ConsumerSP;
|
||||
IAudioLinkFactory::FAudioLinkSourcePushedCreateArgs CreateArgs;
|
||||
|
||||
FFMODAudioLinkSourcePushed(const IAudioLinkFactory::FAudioLinkSourcePushedCreateArgs& InArgs, IAudioLinkFactory* InFactory);
|
||||
virtual ~FFMODAudioLinkSourcePushed() override;
|
||||
void OnNewBuffer(const FOnNewBufferParams& InArgs) override;
|
||||
void OnSourceDone(const int32 InSourceId) override;
|
||||
void OnSourceReleased(const int32 InSourceId) override;
|
||||
void OnUpdateWorldState(const FOnUpdateWorldStateParams& InParams) override;
|
||||
};
|
@ -0,0 +1,141 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "FMODAudioLinkSynchronizer.h"
|
||||
#include "fmod_studio.hpp"
|
||||
#include "FMODStudioModule.h"
|
||||
#include "FMODAudioLinkLog.h"
|
||||
|
||||
#include "AudioDeviceManager.h"
|
||||
|
||||
FMOD_RESULT F_CALLBACK MixCallback(FMOD_SYSTEM* system, FMOD_SYSTEM_CALLBACK_TYPE type, void* commanddata1, void* commanddata2, void* userdata)
|
||||
{
|
||||
FFMODAudioLinkSynchronizer *Synchro = static_cast<FFMODAudioLinkSynchronizer*>(userdata);
|
||||
if (Synchro)
|
||||
{
|
||||
FMOD::System *CoreSystem = (FMOD::System*)system;
|
||||
|
||||
FMOD::ChannelGroup *MasterGroup = NULL;
|
||||
CoreSystem->getMasterChannelGroup(&MasterGroup);
|
||||
uint64 dspClock = 0;
|
||||
MasterGroup->getDSPClock(&dspClock, 0);
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case FMOD_SYSTEM_CALLBACK_POSTMIX:
|
||||
Synchro->ExecuteEndRender(dspClock);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FMOD_OK;
|
||||
}
|
||||
|
||||
FFMODAudioLinkSynchronizer::FFMODAudioLinkSynchronizer()
|
||||
{
|
||||
Bind();
|
||||
}
|
||||
|
||||
FFMODAudioLinkSynchronizer::~FFMODAudioLinkSynchronizer()
|
||||
{
|
||||
Unbind();
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::ExecuteEndRender(uint64 dspClock)
|
||||
{
|
||||
FScopeLock Lock(&CallbackLock);
|
||||
FOnRenderParams Params;
|
||||
Params.BufferTickID = dspClock;
|
||||
Params.NumFrames = dspBufferSize;
|
||||
OnEndRender.Broadcast(Params);
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::ExecuteOpenStream()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkSynchronizer::ExecuteOpenStream: Opening stream between Unreal and FMOD."));
|
||||
|
||||
int samplerate = 0, numchannels = 0;
|
||||
FMOD_SPEAKERMODE speakerMode;
|
||||
CoreSystem->getDSPBufferSize(&dspBufferSize, 0);
|
||||
CoreSystem->getSoftwareFormat(&samplerate, &speakerMode, 0);
|
||||
CoreSystem->getSpeakerModeChannels(speakerMode, &numchannels);
|
||||
|
||||
FOnOpenStreamParams Params;
|
||||
Params.NumChannels = numchannels;
|
||||
Params.SampleRate = samplerate;
|
||||
Params.NumFrames = dspBufferSize;
|
||||
Params.Name = TEXT("AudioLink for FMOD");
|
||||
OnOpenStream.Broadcast(Params);
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::ExecuteCloseStream()
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkSynchronizer::ExecuteCloseStream: Closing stream between Unreal and FMOD."));
|
||||
|
||||
OnCloseStream.Broadcast();
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::ExecuteSuspend()
|
||||
{
|
||||
OnSuspend.Broadcast();
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::ExecuteResume()
|
||||
{
|
||||
OnResume.Broadcast();
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::Bind()
|
||||
{
|
||||
if (UNLIKELY(bIsBound))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkSynchronizer::Bind: Already bound."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (UNLIKELY(!IFMODStudioModule::Get().IsAvailable()))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAduioLinkSynchronizer::Bind: FMODStudioModule not available."));
|
||||
return;
|
||||
}
|
||||
|
||||
auto* StudioSystem = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Runtime);
|
||||
if (!StudioSystem)
|
||||
{
|
||||
StudioSystem = IFMODStudioModule::Get().GetStudioSystem(EFMODSystemContext::Auditioning);
|
||||
}
|
||||
if (UNLIKELY(!StudioSystem))
|
||||
{
|
||||
UE_LOG(LogFMODAudioLink, Error, TEXT("FFMODAudioLinkSynchronizer::ExecuteOpenStream: No Studio System."));
|
||||
return;
|
||||
}
|
||||
|
||||
StudioSystem->getCoreSystem(&CoreSystem);
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkSynchronizer::Bind: Binding System Callbacks."));
|
||||
|
||||
CoreSystem->setUserData(this);
|
||||
CoreSystem->setCallback(MixCallback, FMOD_SYSTEM_CALLBACK_POSTMIX);
|
||||
|
||||
ExecuteOpenStream();
|
||||
|
||||
bIsBound = true;
|
||||
}
|
||||
|
||||
void FFMODAudioLinkSynchronizer::Unbind()
|
||||
{
|
||||
if (UNLIKELY(!bIsBound))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (UNLIKELY(!CoreSystem))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UE_LOG(LogFMODAudioLink, Verbose, TEXT("FFMODAudioLinkSynchronizer::Bind: Unbinding."));
|
||||
CoreSystem->setCallback(NULL, 0);
|
||||
bIsBound = false;
|
||||
|
||||
ExecuteCloseStream();
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "IAudioLink.h"
|
||||
#include "Misc/ScopeRWLock.h"
|
||||
#include "fmod.hpp"
|
||||
|
||||
struct FFMODAudioLinkSynchronizer : IAudioLinkSynchronizer, TSharedFromThis<FFMODAudioLinkSynchronizer, ESPMode::ThreadSafe>
|
||||
{
|
||||
IAudioLinkSynchronizer::FOnSuspend OnSuspend;
|
||||
IAudioLinkSynchronizer::FOnResume OnResume;
|
||||
IAudioLinkSynchronizer::FOnOpenStream OnOpenStream;
|
||||
IAudioLinkSynchronizer::FOnCloseStream OnCloseStream;
|
||||
IAudioLinkSynchronizer::FOnBeginRender OnBeginRender;
|
||||
IAudioLinkSynchronizer::FOnEndRender OnEndRender;
|
||||
|
||||
FRWLock RwLock;
|
||||
FCriticalSection CallbackLock;
|
||||
FMOD::System* CoreSystem;
|
||||
|
||||
bool bIsBound{ false };
|
||||
unsigned int dspBufferSize;
|
||||
|
||||
FFMODAudioLinkSynchronizer();
|
||||
~FFMODAudioLinkSynchronizer() override;
|
||||
|
||||
void ExecuteEndRender(uint64 dspClock);
|
||||
void ExecuteOpenStream();
|
||||
void ExecuteCloseStream();
|
||||
void ExecuteSuspend();
|
||||
void ExecuteResume();
|
||||
void Bind();
|
||||
void Unbind();
|
||||
|
||||
#define MAKE_DELEGATE_FUNC(X)\
|
||||
FDelegateHandle Register##X##Delegate(const FOn##X::FDelegate& InDelegate) override\
|
||||
{\
|
||||
FWriteScopeLock WriteLock(RwLock);\
|
||||
return On##X.Add(InDelegate);\
|
||||
}\
|
||||
bool Remove##X##Delegate(const FDelegateHandle& InHandle) override\
|
||||
{\
|
||||
FWriteScopeLock WriteLock(RwLock);\
|
||||
return On##X.Remove(InHandle);\
|
||||
}
|
||||
|
||||
MAKE_DELEGATE_FUNC(Suspend)
|
||||
MAKE_DELEGATE_FUNC(Resume)
|
||||
MAKE_DELEGATE_FUNC(OpenStream)
|
||||
MAKE_DELEGATE_FUNC(CloseStream)
|
||||
MAKE_DELEGATE_FUNC(BeginRender) //Use to sync UE and FMOD
|
||||
MAKE_DELEGATE_FUNC(EndRender) //Use to sync UE and FMOD
|
||||
|
||||
#undef MAKE_DELEGATE_FUNC
|
||||
|
||||
friend FMOD_RESULT F_CALLBACK MixCallback(FMOD_SYSTEM* system, FMOD_SYSTEM_CALLBACK_TYPE type, void* commanddata1, void* commanddata2, void* userdata);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FMODAudioLinkSettings.h"
|
||||
#include "IAudioLink.h"
|
||||
#include "IAudioLinkBlueprintInterface.h"
|
||||
|
||||
class UAudioComponent;
|
||||
|
||||
#include "FMODAudioLinkComponent.generated.h"
|
||||
|
||||
UCLASS(ClassGroup = (Audio, Common), HideCategories = (Object, ActorComponent, Physics, Rendering, Mobility, LOD),
|
||||
ShowCategories = Trigger, meta = (BlueprintSpawnableComponent))
|
||||
class UFMODAudioLinkComponent : public USceneComponent, public IAudioLinkBlueprintInterface
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AudioLink")
|
||||
TObjectPtr<UFMODAudioLinkSettings> Settings;
|
||||
|
||||
/** The sound to be played */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Sound)
|
||||
TObjectPtr<class USoundBase> Sound;
|
||||
|
||||
protected:
|
||||
//~ Begin IAudioLinkInterface
|
||||
virtual void SetLinkSound(USoundBase* NewSound) override;
|
||||
virtual void PlayLink(float StartTime = 0.0f) override;
|
||||
virtual void StopLink() override;
|
||||
virtual bool IsLinkPlaying() const override;
|
||||
//~ End IAudioLinkInterface
|
||||
|
||||
//~ Begin ActorComponent Interface.
|
||||
virtual void OnRegister() override;
|
||||
virtual void OnUnregister() override;
|
||||
//~ End ActorComponent Interface.
|
||||
|
||||
void CreateAudioComponent();
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UAudioComponent> AudioComponent;
|
||||
|
||||
void CreateLink();
|
||||
TUniquePtr<IAudioLink> LinkInstance;
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Stats/Stats.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
FMODSTUDIO_API DECLARE_LOG_CATEGORY_EXTERN(LogFMODAudioLink, Log, All);
|
@ -0,0 +1,21 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#include "Modules/ModuleInterface.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
#include "FMODAudioLinkFactory.h"
|
||||
|
||||
class FFMODAudioLinkModule
|
||||
{
|
||||
public:
|
||||
FFMODAudioLinkModule()
|
||||
{
|
||||
Factory = MakeUnique<FFMODAudioLinkFactory>();
|
||||
}
|
||||
~FFMODAudioLinkModule()
|
||||
{
|
||||
Factory.Reset();
|
||||
}
|
||||
private:
|
||||
TUniquePtr<FFMODAudioLinkFactory> Factory;
|
||||
};
|
@ -0,0 +1,98 @@
|
||||
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AudioLinkSettingsAbstract.h"
|
||||
#include "Engine/StreamableManager.h"
|
||||
|
||||
#include "FMODAudioLinkSettings.generated.h"
|
||||
|
||||
class UFMODAudioLinkSettings;
|
||||
class UFMODEvent;
|
||||
|
||||
class FFMODAudioLinkSettingsProxy : public IAudioLinkSettingsProxy
|
||||
{
|
||||
public:
|
||||
FFMODAudioLinkSettingsProxy(const UFMODAudioLinkSettings&);
|
||||
virtual ~FFMODAudioLinkSettingsProxy() = default;
|
||||
|
||||
const TSoftObjectPtr<UFMODEvent>& GetLinkEvent() const { return FMODLinkEvent; }
|
||||
int32 GetReceivingBufferSizeInFrames() const { return ReceivingBufferSizeInFrames; }
|
||||
bool ShouldClearBufferOnReceipt() const { return bShouldZeroBuffer; }
|
||||
float GetProducerConsumerBufferRatio() const { return ProducerToConsumerBufferRatio; }
|
||||
float GetInitialSilenceFillRatio() const { return InitialSilenceFillRatio; }
|
||||
|
||||
void Update(const UFMODAudioLinkSettings&);
|
||||
|
||||
bool IsEventDataLoaded() const;
|
||||
void RegisterCallback(const TFunction<void()>& InCallback, FDelegateHandle& OutHandle);
|
||||
bool UnRegisterCallback(const FDelegateHandle& InDelegate);
|
||||
|
||||
protected:
|
||||
void NotifyEventDataLoaded();
|
||||
|
||||
private:
|
||||
#if WITH_EDITOR
|
||||
void RefreshFromSettings(UAudioLinkSettingsAbstract* InSettings, FPropertyChangedEvent& InPropertyChangedEvent) override;
|
||||
#endif //WITH_EDITOR
|
||||
|
||||
FSimpleMulticastDelegate OnEventLoadedDelegate;
|
||||
FCriticalSection CS;
|
||||
friend class UFMODAudioLinkSettings;
|
||||
|
||||
TSoftObjectPtr<UFMODEvent> FMODLinkEvent;
|
||||
int32 ReceivingBufferSizeInFrames;
|
||||
bool bShouldZeroBuffer = true;
|
||||
bool bIsEventDataLoaded = false;
|
||||
float ProducerToConsumerBufferRatio = 2.0f;
|
||||
float InitialSilenceFillRatio = 1.0f;
|
||||
};
|
||||
|
||||
using FSharedFMODAudioLinkSettingsProxyPtr = TSharedPtr<FFMODAudioLinkSettingsProxy, ESPMode::ThreadSafe>;
|
||||
|
||||
UCLASS(config = Engine, defaultconfig)
|
||||
class FMODSTUDIO_API UFMODAudioLinkSettings : public UAudioLinkSettingsAbstract
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(Config, EditAnywhere, Category = default)
|
||||
TSoftObjectPtr<UFMODEvent> LinkEvent;
|
||||
|
||||
/** If this is set, the receiving code will clear the buffer after it's read, so it's not rendered by Unreal. Only applies if running both renderers at once. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = default)
|
||||
bool bShouldClearBufferOnReceipt = true;
|
||||
|
||||
/** This is the ratio of producer to consumer buffer size, 2.0 means its twice as big as the consumer buffer. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = default)
|
||||
float ProducerToConsumerBufferRatio = 2.0f;
|
||||
|
||||
/** Ratio of initial buffer to fill with silence ahead of consumption. Adjusting this can resolve starvation at the cost of added latency. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = default)
|
||||
float InitialSilenceFillRatio = 1.0f;
|
||||
|
||||
void RequestLoadLinkEvent() const;
|
||||
|
||||
protected:
|
||||
TSharedPtr<FStreamableHandle> LoadingHandle;
|
||||
|
||||
/** Once the SoftObjectReference has been resolved, attach the reference here so it's owned. */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UFMODEvent> LinkEventResolved;
|
||||
|
||||
void PostLoad() override; // Only occurs at editor startup and game startup, not PIE
|
||||
void OnLoadCompleteCallback();
|
||||
void FinishDestroy() override;
|
||||
mutable bool bLoadRequested = false;
|
||||
|
||||
friend class FFMODAudioLinkSettingsProxy;
|
||||
|
||||
int32 GetReceivingBufferSizeInFrames() const;
|
||||
|
||||
UAudioLinkSettingsAbstract::FSharedSettingsProxyPtr MakeProxy() const override
|
||||
{
|
||||
return UAudioLinkSettingsAbstract::FSharedSettingsProxyPtr(new FFMODAudioLinkSettingsProxy{ *this });
|
||||
}
|
||||
|
||||
FName GetFactoryName() const override;
|
||||
};
|
||||
|
Reference in New Issue
Block a user