newly added updated FMODStudio plugin for migration

This commit is contained in:
Ji Yoon Rhee
2025-02-02 00:16:31 +09:00
parent 38b2151bb3
commit 2467ad7d4c
171 changed files with 2900 additions and 0 deletions

View File

@ -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);
}
}
};

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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 */
};

View File

@ -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;
}

View File

@ -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>;

View File

@ -0,0 +1,5 @@
// Copyright (c), Firelight Technologies Pty, Ltd. 2024-2024.
#include "FMODAudioLinkLog.h"
DEFINE_LOG_CATEGORY(LogFMODAudioLink);

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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();
}

View File

@ -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);
};