315 lines
12 KiB (Stored with Git LFS)
C++
315 lines
12 KiB (Stored with Git LFS)
C++
// 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: SelfSP = %p, LinkEvent = %s, InComponent = %p.")
|
|
, &SelfSP, *LinkEvent.Get()->GetName(), &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;
|
|
} |