// Copyright Epic Games, Inc. All Rights Reserved. #include "MetasoundAudioBusWriterNode.h" #include "AudioMixerDevice.h" #include "AudioBusSubsystem.h" #include "AudioDevice.h" #include "Internationalization/Text.h" #include "MediaPacket.h" #include "MetasoundAudioBuffer.h" #include "MetasoundAudioBus.h" #include "MetasoundAudioBusPrivate.h" #include "MetasoundExecutableOperator.h" #include "MetasoundFacade.h" #include "MetasoundNodeRegistrationMacro.h" #include "MetasoundParamHelper.h" #include "MetasoundStandardNodesCategories.h" #define LOCTEXT_NAMESPACE "MetasoundAudioBusWriterNode" namespace Metasound { namespace AudioBusWriterNode { namespace Inputs { DEFINE_METASOUND_PARAM(AudioBus, "Audio Bus", "Audio Bus Asset."); DEFINE_METASOUND_PARAM(Audio, "In {0}", "Audio input for channel {0}."); } int32 GetCurrentMajorVersion() { return 1; } } int32 AudioBusWriterNodeInitialNumBlocks(int32 BlockSizeFrames, int32 AudioMixerOutputFrames) { // One less block is required because the metasound will write the final block. int32 MaxSizeFrames = FMath::Max(AudioMixerOutputFrames, BlockSizeFrames), MinSizeFrames = FMath::Min(AudioMixerOutputFrames, BlockSizeFrames); return FMath::DivideAndRoundUp(MaxSizeFrames, MinSizeFrames) - 1; } template class TAudioBusWriterOperator : public TExecutableOperator> { public: static const FNodeClassMetadata& GetNodeInfo() { auto InitNodeInfo = []() -> FNodeClassMetadata { FText NodeDisplayName = METASOUND_LOCTEXT_FORMAT("AudioBusWriterDisplayNamePattern", "Audio Bus Writer ({0})", NumChannels); FNodeClassMetadata Info; Info.ClassName = AudioBusWriterNode::GetClassName(); Info.MajorVersion = AudioBusWriterNode::GetCurrentMajorVersion(); Info.MinorVersion = 0; Info.DisplayName = NodeDisplayName; Info.Description = METASOUND_LOCTEXT("AudioBusWriter_Description", "Sends audio data to the audio bus asset."); Info.Author = PluginAuthor; Info.PromptIfMissing = PluginNodeMissingPrompt; Info.DefaultInterface = GetVertexInterface(); Info.CategoryHierarchy.Emplace(NodeCategories::Io); return Info; }; static const FNodeClassMetadata Info = InitNodeInfo(); return Info; } static const FVertexInterface& GetVertexInterface() { using namespace AudioBusWriterNode; auto CreateVertexInterface = []() -> FVertexInterface { FInputVertexInterface InputInterface; InputInterface.Add(TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(Inputs::AudioBus))); for (uint32 i = 0; i < NumChannels; ++i) { InputInterface.Add(TInputDataVertex(METASOUND_GET_PARAM_NAME_WITH_INDEX_AND_METADATA(Inputs::Audio, i))); } FOutputVertexInterface OutputInterface; return FVertexInterface(InputInterface, OutputInterface); }; static const FVertexInterface Interface = CreateVertexInterface(); return Interface; } static TUniquePtr CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults) { using namespace Frontend; using namespace AudioBusWriterNode; const FInputVertexInterfaceData& InputData = InParams.InputData; bool bHasEnvironmentVars = InParams.Environment.Contains(SourceInterface::Environment::DeviceID); bHasEnvironmentVars &= InParams.Environment.Contains(SourceInterface::Environment::AudioMixerNumOutputFrames); bHasEnvironmentVars &= InParams.Environment.Contains(SourceInterface::Environment::TransmitterID); if (bHasEnvironmentVars) { FAudioBusAssetReadRef AudioBusIn = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(Inputs::AudioBus), InParams.OperatorSettings); TArray AudioInputs; for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { AudioInputs.Add(InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME_WITH_INDEX(Inputs::Audio, ChannelIndex), InParams.OperatorSettings)); } FString GraphName; if (InParams.Environment.Contains(SourceInterface::Environment::GraphName)) { GraphName = InParams.Environment.GetValue(SourceInterface::Environment::GraphName); } else { GraphName = TEXT(""); } return MakeUnique>(InParams, MoveTemp(AudioBusIn), MoveTemp(AudioInputs), MoveTemp(GraphName)); } else { UE_LOG(LogMetaSound, Warning, TEXT("Audio bus writer node requires audio device ID '%s', audio mixer num output frames '%s' and transmitter id '%s' environment variables") , *SourceInterface::Environment::DeviceID.ToString(), *SourceInterface::Environment::AudioMixerNumOutputFrames.ToString(), *SourceInterface::Environment::TransmitterID.ToString()); return nullptr; } } TAudioBusWriterOperator(const FBuildOperatorParams& InParams, FAudioBusAssetReadRef InAudioBusAsset, TArray InAudioInputs, FString InGraphName) : AudioBusAsset(MoveTemp(InAudioBusAsset)) , AudioInputs(MoveTemp(InAudioInputs)) { Reset(InParams); } void CreatePatchInput() { using namespace AudioBusPrivate; const FAudioBusProxyPtr& AudioBusProxy = AudioBusAsset->GetAudioBusProxy(); if (AudioBusProxy.IsValid()) { if (AudioBusProxy->NumChannels <= 0) { UE_LOG(LogMetaSound, Warning, TEXT("AudioBusProxy is invalid (NumChannels = %i)."), AudioBusProxy->NumChannels); return; } UAudioBusSubsystem* AudioBusSubsystem = nullptr; if (FAudioDeviceManager* ADM = FAudioDeviceManager::Get()) { if (FAudioDevice* AudioDevice = ADM->GetAudioDeviceRaw(AudioDeviceId)) { AudioBusSubsystem = AudioDevice->GetSubsystem(); check(AudioBusSubsystem); } } if (!AudioBusSubsystem) { return; } AudioBusChannels = FMath::Min(uint32(AudioBusProxy->NumChannels), uint32(EAudioBusChannels::MaxChannelCount)); AudioBusId = AudioBusProxy->AudioBusId; Audio::FAudioBusKey AudioBusKey(AudioBusId); const FString AudioBusWriterNodeBusName = FString::Format(TEXT("_AudioBusWriterNode_AudioBusId_{0}"), { AudioBusId }); AudioBusSubsystem->StartAudioBus(AudioBusKey, AudioBusWriterNodeBusName, AudioBusChannels, false); AudioBusPatchInput = AudioBusSubsystem->AddPatchInputForSoundAndAudioBus(InstanceID, AudioBusKey, BlockSizeFrames, int32(AudioBusChannels)); int32 NumBlocksToPush = InitialNumBlocks(); // Handle case of mismatched sample rate between audio mixer and // metasound. if (EnableResampledAudioBus && (AudioMixerSampleRate != SampleRate) && (AudioMixerSampleRate > 0.f) && (SampleRate > 0.f)) { UE_LOG(LogMetaSound, Warning, TEXT("Using a audio bus writer node is inefficient if the MetaSound sample rate %f does not match the AudioMixer sample rate %f. Please update MetaSound SampleRate to match the AudioMixer's SampleRate"), SampleRate, AudioMixerSampleRate); if (NumBlocksToPush) { // Sample rate matches between audio mixer and metasound. This // node will produce audio to the patch in approximate block // sizes of (BlockSize * SampleRate / AudioMixerSampleRate) AudioBusPatchInput.PushAudio(nullptr, NumBlocksToNumSamples(NumBlocksToPush, SampleRate / AudioMixerSampleRate)); } ResampledPatchInput = MakeUnique(AudioBusChannels, AudioMixerSampleRate, SampleRate, BlockSizeFrames, AudioBusPatchInput); } else if (NumBlocksToPush > 0) { // Sample rate matches between audio mixer and metasound. AudioBusPatchInput.PushAudio(nullptr, NumBlocksToNumSamples(NumBlocksToPush)); } // Allocate and fill the interleaved buffer with silence, // in case it contains more channels than the node supports. InterleavedBuffer.Reset(); InterleavedBuffer.AddZeroed(NumBlocksToNumSamples(1)); } } void Reset(const IOperator::FResetParams& InParams) { using namespace Frontend; using namespace AudioBusWriterNode; InterleavedBuffer.Reset(); AudioMixerOutputFrames = INDEX_NONE; AudioMixerSampleRate = -1.f; AudioDeviceId = INDEX_NONE; SampleRate = InParams.OperatorSettings.GetSampleRate(); AudioBusPatchInput.Reset(); ResampledPatchInput.Reset(); AudioBusChannels = INDEX_NONE; AudioBusId = 0; InstanceID = 0; SampleRate = InParams.OperatorSettings.GetSampleRate(); BlockSizeFrames = InParams.OperatorSettings.GetNumFramesPerBlock(); bWasUnderrunReported = false; bool bHasEnvironmentVars = InParams.Environment.Contains(SourceInterface::Environment::DeviceID); bHasEnvironmentVars &= InParams.Environment.Contains(SourceInterface::Environment::AudioMixerNumOutputFrames); bHasEnvironmentVars &= InParams.Environment.Contains(SourceInterface::Environment::TransmitterID); if (bHasEnvironmentVars) { AudioDeviceId = InParams.Environment.GetValue(SourceInterface::Environment::DeviceID); AudioMixerOutputFrames = InParams.Environment.GetValue(SourceInterface::Environment::AudioMixerNumOutputFrames); InstanceID = InParams.Environment.GetValue(SourceInterface::Environment::TransmitterID); } else { UE_LOG(LogMetaSound, Warning, TEXT("Audio bus writer node requires audio device ID '%s', audio mixer num output frames '%s' and transmitter id '%s' environment variables") , *SourceInterface::Environment::DeviceID.ToString(), *SourceInterface::Environment::AudioMixerNumOutputFrames.ToString(), *SourceInterface::Environment::TransmitterID.ToString()); } // Audio mixer sample rate is a newer addition to the set of required environment variables in UE 5.6. Check separately // and fall back to old behavior if it does not exist. if (InParams.Environment.Contains(SourceInterface::Environment::AudioMixerSampleRate)) { AudioMixerSampleRate = InParams.Environment.GetValue(SourceInterface::Environment::AudioMixerSampleRate); } else { UE_LOG(LogMetaSound, Warning, TEXT("Audio bus writer node may not render correctly without the audio mixer sample rate '%s' environment variable"), *SourceInterface::Environment::AudioMixerSampleRate.ToString()); // Assume matching sample rate if environment variable is missing. AudioMixerSampleRate = SampleRate; } } virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override { using namespace AudioBusWriterNode; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(Inputs::AudioBus), AudioBusAsset); for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME_WITH_INDEX(Inputs::Audio, ChannelIndex), AudioInputs[ChannelIndex]); } } virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override { } void Execute() { const FAudioBusProxyPtr& BusProxy = AudioBusAsset->GetAudioBusProxy(); if (BusProxy.IsValid() && BusProxy->AudioBusId != AudioBusId) { InterleavedBuffer.Reset(); } if (InterleavedBuffer.IsEmpty()) { // if environment vars & a valid audio bus have been set since starting, try to create the patch now if (SampleRate > 0.f && BusProxy.IsValid()) { CreatePatchInput(); } } if (InterleavedBuffer.IsEmpty()) { return; } // Retrieve input and interleaved buffer pointers const float* AudioInputBufferPtrs[NumChannels]; for (uint32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { AudioInputBufferPtrs[ChannelIndex] = AudioInputs[ChannelIndex]->GetData(); } float* InterleavedBufferPtr = InterleavedBuffer.GetData(); if (AudioBusChannels == 1) { FMemory::Memcpy(InterleavedBufferPtr, AudioInputBufferPtrs[0], BlockSizeFrames * sizeof(float)); } else { // Interleave the inputs // Writing the channels of the interleaved buffer sequentially should improve // cache utilization compared to writing each input's frames sequentially. // There is more likely to be a cache line for each buffer than for the // entirety of the interleaved buffer. uint32 MinChannels = FMath::Min(AudioBusChannels, NumChannels); for (int32 FrameIndex = 0; FrameIndex < BlockSizeFrames; ++FrameIndex) { // Fill as many channels in the interleaved buffer as possible, // given the number of available audio buffers. for (uint32 ChannelIndex = 0; ChannelIndex < MinChannels; ++ChannelIndex) { InterleavedBufferPtr[ChannelIndex] = *AudioInputBufferPtrs[ChannelIndex]++; } // The interleaved buffer has as many channels as the assigned audio bus. InterleavedBufferPtr += AudioBusChannels; } } int32 SamplesPushed = -1; if (ResampledPatchInput.IsValid()) { // Perform resampling when pushing audio in case where audio mixer sample rate // does not match the metasound sample rate. SamplesPushed = ResampledPatchInput->PushAudio(InterleavedBuffer.GetData(), InterleavedBuffer.Num()); } else { // Pushes the interleaved data to the audio bus SamplesPushed = AudioBusPatchInput.PushAudio(InterleavedBuffer.GetData(), InterleavedBuffer.Num()); } if (SamplesPushed < InterleavedBuffer.Num() && !bWasUnderrunReported) { UE_LOG(LogMetaSound, Warning, TEXT("Underrun detected in audio bus writer node.")); bWasUnderrunReported = true; } } private: int32 InitialNumBlocks() const { return AudioBusWriterNodeInitialNumBlocks(BlockSizeFrames, AudioMixerOutputFrames); } int32 NumBlocksToNumSamples(int32 NumBlocks, float InSampleRateRatio=-1.f) const { if (InSampleRateRatio > 0.f) { return FMath::CeilToInt(NumBlocks * BlockSizeFrames * InSampleRateRatio) * AudioBusChannels; } else { return NumBlocks * BlockSizeFrames * AudioBusChannels; } } FAudioBusAssetReadRef AudioBusAsset; TArray AudioInputs; Audio::FAlignedFloatBuffer InterleavedBuffer; TUniquePtr ResampledPatchInput; int32 AudioMixerOutputFrames = INDEX_NONE; float AudioMixerSampleRate = -1.f; Audio::FDeviceId AudioDeviceId = INDEX_NONE; float SampleRate = 0.0f; Audio::FPatchInput AudioBusPatchInput; uint64 InstanceID = 0; uint32 AudioBusChannels = INDEX_NONE; uint32 AudioBusId = 0; int32 BlockSizeFrames = 0; bool bWasUnderrunReported = false; }; template using TAudioBusWriterNode = TNodeFacade>; #define REGISTER_AUDIO_BUS_WRITER_NODE(ChannelCount) \ using FAudioBusWriterNode_##ChannelCount = TAudioBusWriterNode; \ METASOUND_REGISTER_NODE(FAudioBusWriterNode_##ChannelCount) \ REGISTER_AUDIO_BUS_WRITER_NODE(1); REGISTER_AUDIO_BUS_WRITER_NODE(2); REGISTER_AUDIO_BUS_WRITER_NODE(4); REGISTER_AUDIO_BUS_WRITER_NODE(6); REGISTER_AUDIO_BUS_WRITER_NODE(8); } #undef LOCTEXT_NAMESPACE