// 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 FFMODAudioLinkFactory::GetSettingsClass() const { return UFMODAudioLinkSettings::StaticClass(); } TUniquePtr 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(); // Make buffer listener first, which is our producer. IAudioLinkFactory::FSubmixBufferListenerCreateParams SubmixListenerCreateArgs; SubmixListenerCreateArgs.SizeOfBufferInFrames = FMODSettingsSP->GetReceivingBufferSizeInFrames(); SubmixListenerCreateArgs.bShouldZeroBuffer = FMODSettingsSP->ShouldClearBufferOnReceipt(); FSharedBufferedOutputPtr ProducerSP = CreateSubmixBufferListener(SubmixListenerCreateArgs); TWeakPtr ProducerWeak(ProducerSP); // Create consumer. FSharedFMODAudioLinkInputClientPtr ConsumerSP = MakeShared( ProducerSP, InArgs.Settings->GetProxy(), InArgs.Submix->GetFName()); TWeakPtr 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(ProducerSP, ConsumerSP, InArgs.Device); } TUniquePtr 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(); // 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 ProducerWeak(ProducerSP); // Create consumer. FSharedFMODAudioLinkInputClientPtr ConsumerSP = MakeShared(ProducerSP, FMODSettingsSP, OwnerName); TWeakPtr 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(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(ProducerSP, ConsumerSP); } IAudioLinkFactory::FAudioLinkSourcePushedSharedPtr FFMODAudioLinkFactory::CreateSourcePushedAudioLink(const FAudioLinkSourcePushedCreateArgs& InArgs) { if (IFMODStudioModule::IsAvailable()) { UE_LOG(LogFMODAudioLink, VeryVerbose, TEXT("FFMODAudioLinkFactory::CreateSourcePushedAudioLink: Create AudioLink SourcePushed.")); return MakeShared(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(); return SynchronizerSP; }