// Copyright Epic Games, Inc. All Rights Reserved. #include "AudioMixerDevice.h" #include "AudioBusSubsystem.h" #include "AudioDevice.h" #include "Internationalization/Text.h" #include "MediaPacket.h" #include "MetasoundAudioBuffer.h" #include "MetasoundAudioBus.h" #include "MetasoundEngineNodesNames.h" #include "MetasoundExecutableOperator.h" #include "MetasoundFacade.h" #include "MetasoundNodeRegistrationMacro.h" #include "MetasoundParamHelper.h" #include "MetasoundStandardNodesCategories.h" #define LOCTEXT_NAMESPACE "MetasoundAudioBusWriterNode" namespace Metasound { namespace AudioBusWriterNode { METASOUND_PARAM(InParamAudioBusOutput, "Audio Bus", "Audio Bus Asset."); METASOUND_PARAM(InParamAudio, "In {0}", "Audio input for channel {0}."); } template class TAudioBusWriterOperator : public TExecutableOperator> { public: static const FNodeClassMetadata& GetNodeInfo() { auto InitNodeInfo = []() -> FNodeClassMetadata { FName OperatorName = *FString::Printf(TEXT("Audio Bus Writer (%d)"), NumChannels); FText NodeDisplayName = METASOUND_LOCTEXT_FORMAT("AudioBusWriterDisplayNamePattern", "Audio Bus Writer ({0})", NumChannels); FNodeClassMetadata Info; Info.ClassName = { EngineNodes::Namespace, OperatorName, TEXT("") }; Info.MajorVersion = 1; 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(InParamAudioBusOutput))); for (uint32 i = 0; i < NumChannels; ++i) { InputInterface.Add(TInputDataVertex(METASOUND_GET_PARAM_NAME_WITH_INDEX_AND_METADATA(InParamAudio, 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); if (bHasEnvironmentVars) { FAudioBusAssetReadRef AudioBusIn = InputData.GetOrConstructDataReadReference(METASOUND_GET_PARAM_NAME(InParamAudioBusOutput)); TArray AudioInputs; for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { AudioInputs.Add(InputData.GetOrConstructDataReadReference(METASOUND_GET_PARAM_NAME_WITH_INDEX(InParamAudio, 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' and audio mixer num output frames '%s' environment variables") , *SourceInterface::Environment::DeviceID.ToString(), *SourceInterface::Environment::AudioMixerNumOutputFrames.ToString()); return nullptr; } } TAudioBusWriterOperator(const FBuildOperatorParams& InParams, FAudioBusAssetReadRef InAudioBusAsset, TArray InAudioInputs, FString InGraphName) : AudioBusAsset(MoveTemp(InAudioBusAsset)) , AudioInputs(MoveTemp(InAudioInputs)) , GraphName(MoveTemp(InGraphName)) { Reset(InParams); } void CreatePatchInput() { const FAudioBusProxyPtr& AudioBusProxy = AudioBusAsset->GetAudioBusProxy(); if (AudioBusProxy.IsValid()) { if (FAudioDeviceManager* ADM = FAudioDeviceManager::Get()) { if (FAudioDevice* AudioDevice = ADM->GetAudioDeviceRaw(AudioDeviceId)) { UAudioBusSubsystem* AudioBusSubsystem = AudioDevice->GetSubsystem(); check(AudioBusSubsystem); AudioBusId = AudioBusProxy->AudioBusId; const Audio::FAudioBusKey AudioBusKey = Audio::FAudioBusKey(AudioBusId); // Start the audio bus in case it's not already started AudioBusChannels = AudioBusProxy->NumChannels; AudioBusSubsystem->StartAudioBus(AudioBusKey, AudioBusChannels, false); InterleavedBuffer.Reset(); InterleavedBuffer.Reserve(NumBlocksToNumSamples(InitialNumBlocks())); // Create a bus patch input with enough room for the number of samples we expect and some buffering AudioBusPatchInput = AudioBusSubsystem->AddPatchInputForAudioBus(AudioBusKey, BlockSizeFrames, AudioBusChannels); ConnectionState = EConnectionState::Disconnected; } } } } void Reset(const IOperator::FResetParams& InParams) { using namespace Frontend; using namespace AudioBusWriterNode; bool bHasEnvironmentVars = InParams.Environment.Contains(SourceInterface::Environment::DeviceID); bHasEnvironmentVars &= InParams.Environment.Contains(SourceInterface::Environment::AudioMixerNumOutputFrames); if (bHasEnvironmentVars) { SampleRate = InParams.OperatorSettings.GetSampleRate(); AudioDeviceId = InParams.Environment.GetValue(SourceInterface::Environment::DeviceID); AudioMixerOutputFrames = InParams.Environment.GetValue(SourceInterface::Environment::AudioMixerNumOutputFrames); } else { UE_LOG(LogMetaSound, Warning, TEXT("Audio bus writer node requires audio device ID '%s' and audio mixer num output frames '%s' environment variables") , *SourceInterface::Environment::DeviceID.ToString(), *SourceInterface::Environment::AudioMixerNumOutputFrames.ToString()); } BlockSizeFrames = InParams.OperatorSettings.GetNumFramesPerBlock(); bWasUnderrunReported = false; CreatePatchInput(); } virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override { using namespace AudioBusWriterNode; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InParamAudioBusOutput), AudioBusAsset); for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME_WITH_INDEX(InParamAudio, ChannelIndex), AudioInputs[ChannelIndex]); } } virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override { } virtual FDataReferenceCollection GetInputs() const override { // This should never be called. Bind(...) is called instead. This method // exists as a stop-gap until the API can be deprecated and removed. checkNoEntry(); return {}; } virtual FDataReferenceCollection GetOutputs() const override { // This should never be called. Bind(...) is called instead. This method // exists as a stop-gap until the API can be deprecated and removed. checkNoEntry(); return {}; } void Execute() { const FAudioBusProxyPtr& BusProxy = AudioBusAsset->GetAudioBusProxy(); if (BusProxy.IsValid() && BusProxy->AudioBusId != AudioBusId) { AudioBusPatchInput.Reset(); } if (!AudioBusPatchInput.IsValid()) { // 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(); } else { return; } } if (!AudioBusPatchInput.IsOutputStillActive()) { return; } // Retrieve input and interleaved buffer pointers const float* AudioInputBufferPtrs[NumChannels]; for (uint32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex) { AudioInputBufferPtrs[ChannelIndex] = AudioInputs[ChannelIndex]->GetData(); } int32 InterleavedBufferOffset = InterleavedBuffer.Num(); InterleavedBuffer.SetNum(InterleavedBufferOffset + BlockSizeFrames * NumChannels); float* InterleavedBufferPtr = InterleavedBuffer.GetData() + InterleavedBufferOffset; 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) { for (uint32 ChannelIndex = 0; ChannelIndex < MinChannels; ++ChannelIndex) { InterleavedBufferPtr[ChannelIndex] = *AudioInputBufferPtrs[ChannelIndex]++; } InterleavedBufferPtr += AudioBusChannels; } } switch (ConnectionState) { case EConnectionState::Disconnected: { // Wait until InterleavedBuffer contains enough samples to satisfy any mix eventualities! // A single mix requires enough MetaSound executions to satisfy its buffer size. // Mixing can occur concurrently with the first MetaSound execution intended for the next mix, // and can steal that execution's patch output. if (InterleavedBuffer.Num() < NumBlocksToNumSamples(InitialNumBlocks())) { return; } ConnectionState = EConnectionState::ConnectionPending; break; } case EConnectionState::ConnectionPending: { int32 InitialNumSamples = NumBlocksToNumSamples(InitialNumBlocks()); // Determine if the pending connection has been established, by detecting if samples have been consumed. if (AudioBusPatchInput.GetNumSamplesAvailable() == InitialNumSamples) { // If the connection hasn't been established by the time as many executions have occurred again, // something has probably gone wrong. if (InterleavedBuffer.Num() == InitialNumSamples) { UE_LOG(LogMetaSound, Warning, TEXT("Graph %s: Writer node executed before mixer patch connection established, with buffer size %d."), *GraphName, InterleavedBuffer.Num()); return; } return; } ConnectionState = EConnectionState::Connected; break; } } // Pushes the interleaved data to the audio bus const int32 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; } InterleavedBuffer.Reset(); } private: int32 InitialNumBlocks() const { if (AudioMixerOutputFrames == BlockSizeFrames) { return 1; } // We need enough blocks for as many executions as the mixer can consume at once, plus one more, // because the last execution can be concurrent with the mixer, and could contribute to either the // current mix or the next. That could leave us with one block too few if we didn't have an extra. int32 MaxSizeFrames = FMath::Max(AudioMixerOutputFrames, BlockSizeFrames), MinSizeFrames = FMath::Min(AudioMixerOutputFrames, BlockSizeFrames); return 1 + FMath::DivideAndRoundUp(MaxSizeFrames, MinSizeFrames); } int32 NumBlocksToNumSamples(int32 NumBlocks) const { return NumBlocks * BlockSizeFrames * AudioBusChannels; } FAudioBusAssetReadRef AudioBusAsset; TArray AudioInputs; FString GraphName; TArray InterleavedBuffer; int32 AudioMixerOutputFrames = INDEX_NONE; Audio::FDeviceId AudioDeviceId = INDEX_NONE; float SampleRate = 0.0f; Audio::FPatchInput AudioBusPatchInput; uint32 AudioBusChannels = INDEX_NONE; uint32 AudioBusId = 0; bool bWasUnderrunReported = false; int32 BlockSizeFrames = 0; enum class EConnectionState : uint8 { Disconnected, ConnectionPending, Connected } ConnectionState = EConnectionState::Disconnected; }; template class TAudioBusWriterNode : public FNodeFacade { public: TAudioBusWriterNode(const FNodeInitData& InitData) : FNodeFacade(InitData.InstanceName, InitData.InstanceID, TFacadeOperatorClass>()) { } }; #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