// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/MultithreadedPatching.h" #include "DSP/FloatArrayMath.h" #include "HAL/IConsoleManager.h" #include "HAL/PlatformProcess.h" #include "HAL/PlatformTime.h" #include "HAL/Event.h" static int32 MultithreadedPatchingPushCallsPerOutputCleanupCheckCVar = 256; FAutoConsoleVariableRef CVarMultithreadedPatchingPushCallsPerOutputCleanupCheck( TEXT("au.MultithreadedPatching.PushCallsPerOutputCleanupCheck"), MultithreadedPatchingPushCallsPerOutputCleanupCheckCVar, TEXT("Number of push calls (usually corrisponding to audio block updates)\n") TEXT("before checking if an output is ready to be destroyed. Default = 256"), ECVF_Default); namespace Audio { TAtomic FPatchOutput::PatchIDCounter(0); FPatchOutput::FPatchOutput(int32 InMaxCapacity, float InGain /*= 1.0f*/) : InternalBuffer(InMaxCapacity) , TargetGain(InGain) , PreviousGain(InGain) , PatchID(++PatchIDCounter) , NumAliveInputs(0) , SamplesPushedEvent(nullptr) { } FPatchOutput::FPatchOutput() : InternalBuffer(0) , TargetGain(0.0f) , PreviousGain(0.0f) , PatchID(INDEX_NONE) , NumAliveInputs(0) , SamplesPushedEvent(nullptr) { } FPatchOutput::~FPatchOutput() { if (FEvent* Event = SamplesPushedEvent.exchange(nullptr, std::memory_order_acq_rel)) { FPlatformProcess::ReturnSynchEventToPool(Event); } } int32 FPatchOutput::PopAudio(float* OutBuffer, int32 NumSamples, bool bUseLatestAudio) { if (IsInputStale()) { return INDEX_NONE; } const int32 CurrSamplesAvailable = GetNumSamplesAvailable(); const int32 CurrCapacity = InternalBuffer.GetCapacity(); const int32 CurrSamplesFilled = CurrCapacity - CurrSamplesAvailable; if (bUseLatestAudio && InternalBuffer.Num() > (uint32) NumSamples) { InternalBuffer.SetNum((uint32)NumSamples); } int32 PopResult = InternalBuffer.Pop(OutBuffer, NumSamples); TArrayView OutBufferView(OutBuffer, PopResult); // Apply gain stage. float TG = TargetGain, PG = PreviousGain; if (FMath::IsNearlyEqual(TG, PG)) { ArrayMultiplyByConstantInPlace(OutBufferView, PreviousGain); } else { ArrayFade(OutBufferView, PreviousGain, TargetGain); PreviousGain = TargetGain; } return PopResult; } bool FPatchOutput::IsInputStale() const { return NumAliveInputs.load(std::memory_order_relaxed) == 0; } int32 FPatchOutput::PushAudioToInternalBuffer(const float* InBuffer, int32 NumSamples) { const int32 NumSamplesPushed = InBuffer ? InternalBuffer.Push(InBuffer, NumSamples) : InternalBuffer.PushZeros(NumSamples); // Check to see if we need to notify anybody waiting on this patch output getting filled if (FEvent* Event = SamplesPushedEvent.load(std::memory_order_acquire)) { Event->Trigger(); } const int32 CurrSamplesAvailable = GetNumSamplesAvailable(); const int32 CurrCapacity = InternalBuffer.GetCapacity(); const int32 CurrSamplesFilled = CurrCapacity - CurrSamplesAvailable; return NumSamplesPushed; } void FPatchOutput::EmptyInternalBuffer() { // Don't do an Empty() because then we would need to re-initialize. InternalBuffer.Pop(GetNumSamplesAvailable()); } int32 FPatchOutput::MixInAudio(float* OutBuffer, int32 NumSamples, bool bUseLatestAudio) { if (IsInputStale()) { return INDEX_NONE; } MixingBuffer.SetNumUninitialized(NumSamples, EAllowShrinking::No); int32 PopResult = 0; if (bUseLatestAudio && InternalBuffer.Num() > (uint32)NumSamples) { InternalBuffer.SetNum(((uint32)NumSamples)); PopResult = InternalBuffer.Peek(MixingBuffer.GetData(), NumSamples); } else { PopResult = InternalBuffer.Pop(MixingBuffer.GetData(), NumSamples); } TArrayView MixingBufferView(MixingBuffer.GetData(), PopResult); TArrayView OutBufferView(OutBuffer, PopResult); float TG = TargetGain, PG = PreviousGain; if (FMath::IsNearlyEqual(TG, PG)) { ArrayMixIn(MixingBufferView, OutBufferView, PreviousGain); } else { ArrayMixIn(MixingBufferView, OutBufferView, PreviousGain, TargetGain); PreviousGain = TargetGain; } return PopResult; } int32 FPatchOutput::GetNumSamplesAvailable() const { return InternalBuffer.Num(); } bool FPatchOutput::WaitUntilNumSamplesAvailable(int32 InNumSamplesToWaitFor, uint32 TimeOutMilliseconds) { // Samples are ready if there are enough of them available, or the input is stale. // Conceptually, a stale input contributes silence, so its samples are known. if (IsInputStale() || GetNumSamplesAvailable() >= InNumSamplesToWaitFor) { return true; } // Samples are not ready if the timeout is zero or the // internal buffer can't hold the number of requested samples. if (!TimeOutMilliseconds || InternalBuffer.GetCapacity() < uint32(InNumSamplesToWaitFor)) { return false; } // Determine if an event is ready to signal when // samples are submitted. Provide one if not. FEvent* Event = SamplesPushedEvent.load(std::memory_order_acquire); if (!Event) { FEvent* NewEvent = FPlatformProcess::GetSynchEventFromPool(false); if (SamplesPushedEvent.compare_exchange_strong(Event, NewEvent, std::memory_order_release, std::memory_order_acquire)) { // Use the new event. Event = NewEvent; } else { // Use the existing event and return the new one. FPlatformProcess::ReturnSynchEventToPool(NewEvent); } } // Calculate when the timeout period will end. double CurrentTime = FPlatformTime::Seconds(); double WaitStopTime = CurrentTime + 0.001 * TimeOutMilliseconds; // First determine if samples are available. // They might have become available before the event was ready to signal. while (!IsInputStale() && GetNumSamplesAvailable() < InNumSamplesToWaitFor) { // Wait for the next sample submission. Event->Wait(TimeOutMilliseconds); // If the timeout is not infinite, update it. if (TimeOutMilliseconds != MAX_uint32) { CurrentTime = FPlatformTime::Seconds(); // Return if no time remains, indicating whether samples are ready. double TimeOutRemainder = (WaitStopTime - CurrentTime) * 1000.; if (!(TimeOutRemainder > 0.)) { return IsInputStale() || GetNumSamplesAvailable() >= InNumSamplesToWaitFor; } // Truncating to an integer will cause a spin-wait // when there's less than a millisecond remaining. TimeOutMilliseconds = uint32(TimeOutRemainder); } } // Samples are ready. return true; } FPatchInput::FPatchInput(const FPatchOutputStrongPtr& InOutput) : OutputHandle(InOutput) { if (OutputHandle.IsValid()) { OutputHandle->NumAliveInputs.fetch_add(1, std::memory_order_relaxed); } } FPatchInput::FPatchInput(const FPatchInput& InOther) : OutputHandle(InOther.OutputHandle) { if (OutputHandle.IsValid()) { OutputHandle->NumAliveInputs.fetch_add(1, std::memory_order_relaxed); } } FPatchInput::FPatchInput(FPatchInput&& Other) : OutputHandle(MoveTemp(Other.OutputHandle)) , PushCallsCounter(Other.PushCallsCounter) { Other.PushCallsCounter = 0; } FPatchInput& FPatchInput::operator=(const FPatchInput& Other) { FPatchInput NewPatchInput(Other); Swap(NewPatchInput, *this); return *this; } FPatchInput& FPatchInput::operator=(FPatchInput&& Other) { FPatchInput NewPatchInput(MoveTemp(Other)); Swap(NewPatchInput, *this); return *this; } FPatchInput::~FPatchInput() { if (OutputHandle.IsValid()) { if (OutputHandle->NumAliveInputs.fetch_sub(1, std::memory_order_relaxed) == 1) { if (FEvent* Event = OutputHandle->SamplesPushedEvent.load(std::memory_order_acquire)) { // Signal that the sample status should be checked. Event->Trigger(); } } } } int32 FPatchInput::PushAudio(const float* InBuffer, int32 NumSamples) { if (!OutputHandle.IsValid()) { return INDEX_NONE; } int32 SamplesPushed = OutputHandle->PushAudioToInternalBuffer(InBuffer, NumSamples); // Periodically check to see if the output handle has been destroyed and clean it up. // If the buffer is full, check as well to determine if it is possibly due to the // output going stale between periodic push call counter checks. const int32 PushCallsPerOutputCleanupCheck = FMath::Max(1, MultithreadedPatchingPushCallsPerOutputCleanupCheckCVar); PushCallsCounter = (PushCallsCounter + 1) % PushCallsPerOutputCleanupCheck; if (PushCallsCounter == 0 || SamplesPushed == 0) { if (OutputHandle.IsUnique()) { // Deletes the output as it is the last remaining handle OutputHandle.Reset(); SamplesPushed = INDEX_NONE; } } return SamplesPushed; } void FPatchInput::ClearAudio() { if (!OutputHandle.IsValid()) { return; } OutputHandle->EmptyInternalBuffer(); } /** Returns the current number of samples buffered in this input. */ int32 FPatchInput::GetNumSamplesAvailable() const { if (!OutputHandle.IsValid()) { return 0; } return OutputHandle->GetNumSamplesAvailable(); } void FPatchInput::SetGain(float InGain) { if (!OutputHandle.IsValid()) { return; } OutputHandle->TargetGain = InGain; } bool FPatchInput::IsOutputStillActive() const { return OutputHandle.IsValid() && !OutputHandle.IsUnique(); } bool FPatchInput::IsValid() const { return OutputHandle.IsValid(); } void FPatchInput::Reset() { OutputHandle.Reset(); } FPatchInput FPatchMixer::AddNewInput(int32 InMaxLatencyInSamples, float InGain) { FScopeLock ScopeLock(&PendingNewInputsCriticalSection); const int32 NewPatchIndex = PendingNewInputs.Add(MakeShared(InMaxLatencyInSamples, InGain)); return FPatchInput(PendingNewInputs[NewPatchIndex]); } void FPatchMixer::AddNewInput(const FPatchInput& InPatchInput) { if (!InPatchInput.OutputHandle.IsValid()) { return; } FScopeLock ScopeLock(&PendingNewInputsCriticalSection); PendingNewInputs.Add(InPatchInput.OutputHandle); } void FPatchMixer::RemovePatch(const FPatchInput& InPatchInput) { // If the output is already disconnected, early exit. if (!InPatchInput.OutputHandle.IsValid()) { return; } FScopeLock ScopeLock(&InputDeletionCriticalSection); DisconnectedInputs.Add(InPatchInput.OutputHandle->PatchID); } int32 FPatchMixer::PopAudio(float* OutBuffer, int32 OutNumSamples, bool bUseLatestAudio) { FScopeLock ScopeLock(&CurrentPatchesCriticalSection); CleanUpDisconnectedPatches(); ConnectNewPatches(); FMemory::Memzero(OutBuffer, OutNumSamples * sizeof(float)); int32 MaxPoppedSamples = 0; for (FPatchOutputStrongPtr& OutputPtr : CurrentInputs) { const int32 NumPoppedSamples = OutputPtr->MixInAudio(OutBuffer, OutNumSamples, bUseLatestAudio); MaxPoppedSamples = FMath::Max(NumPoppedSamples, MaxPoppedSamples); } return MaxPoppedSamples; } int32 FPatchMixer::Num() { FScopeLock ScopeLock(&CurrentPatchesCriticalSection); CleanUpDisconnectedPatches(); ConnectNewPatches(); return CurrentInputs.Num(); } int32 FPatchMixer::MaxNumberOfSamplesThatCanBePopped() { FScopeLock ScopeLock(&CurrentPatchesCriticalSection); CleanUpDisconnectedPatches(); ConnectNewPatches(); if (CurrentInputs.IsEmpty()) { return INDEX_NONE; } // Iterate through our inputs and see which input has the least audio buffered. uint32 SmallestNumSamplesBuffered = TNumericLimits::Max(); for (FPatchOutputStrongPtr& Output : CurrentInputs) { if (Output.IsValid()) { SmallestNumSamplesBuffered = FMath::Min(SmallestNumSamplesBuffered, Output->InternalBuffer.Num()); } } return SmallestNumSamplesBuffered; } bool FPatchMixer::WaitUntilNumSamplesAvailable(int32 NumSamples, uint32 TimeOutMilliseconds) { FScopeLock ScopeLock(&CurrentPatchesCriticalSection); CleanUpDisconnectedPatches(); ConnectNewPatches(); if (CurrentInputs.IsEmpty()) { // Samples are ready if there are enough of them available, or the input is stale. // Conceptually, a stale input contributes silence, so its samples are known. return true; } // Calculate when the timeout period will end. double CurrentTime = FPlatformTime::Seconds(); double WaitStopTime = CurrentTime + 0.001 * TimeOutMilliseconds; for (FPatchOutputStrongPtr& Output : CurrentInputs) { bool bSamplesAvailable; do { bSamplesAvailable = Output->WaitUntilNumSamplesAvailable(NumSamples, TimeOutMilliseconds); // If the timeout is not infinite, update it. if (TimeOutMilliseconds != MAX_uint32) { CurrentTime = FPlatformTime::Seconds(); // Return if no time remains and samples are not ready. double TimeOutRemainder = (WaitStopTime - CurrentTime) * 1000.; if (!bSamplesAvailable && !(TimeOutRemainder > 0.)) { return false; } // If samples were ready, but the timeout has expired, // the wait could still succeed if all the subsequent inputs are ready. TimeOutMilliseconds = TimeOutRemainder > 0. ? uint32(TimeOutRemainder) : 0; } } while (!bSamplesAvailable); } return true; } void FPatchMixer::DisconnectAllInputs() { FScopeLock ScopeLock(&CurrentPatchesCriticalSection); CurrentInputs.Reset(); } void FPatchMixer::ConnectNewPatches() { FScopeLock ScopeLock(&PendingNewInputsCriticalSection); // If AddNewPatch is called in a separate thread, wait until the next PopAudio call to do this work. // Todo: convert this to move semantics to avoid copying the shared pointer around. for (FPatchOutputStrongPtr& Patch : PendingNewInputs) { CurrentInputs.Add(Patch); } PendingNewInputs.Reset(); } void FPatchMixer::CleanUpDisconnectedPatches() { FScopeLock PendingInputDeletionScopeLock(&InputDeletionCriticalSection); // Callers of this function must have CurrentPatchesCriticalSection locked so that // this is not causing a race condition. for (const FPatchOutputStrongPtr& Patch : CurrentInputs) { check(Patch.IsValid()); if (Patch->IsInputStale()) { DisconnectedInputs.Add(Patch->PatchID); } } // Iterate through all of the PatchIDs to be cleaned up. for (const int32& PatchID : DisconnectedInputs) { bool bInputRemoved = false; // First, make sure that the patch isn't in the pending new patchs we haven't added yet: { FScopeLock PendingNewInputsScopeLock(&PendingNewInputsCriticalSection); for (int32 Index = 0; Index < PendingNewInputs.Num(); Index++) { checkSlow(CurrentInputs[Index].IsValid()); if (PatchID == PendingNewInputs[Index]->PatchID) { PendingNewInputs.RemoveAtSwap(Index); bInputRemoved = true; break; } } } if (bInputRemoved) { continue; } // Disconnect stale patches. for (int32 Index = 0; Index < CurrentInputs.Num(); Index++) { checkSlow(CurrentInputs[Index].IsValid()); if (PatchID == CurrentInputs[Index]->PatchID) { CurrentInputs.RemoveAtSwap(Index); break; } } } DisconnectedInputs.Reset(); } FPatchOutputStrongPtr FPatchSplitter::AddNewPatch(int32 MaxLatencyInSamples, float InGain) { FPatchOutputStrongPtr StrongOutputPtr = MakeShared(MaxLatencyInSamples * 2, InGain); { FScopeLock ScopeLock(&PendingOutputsCriticalSection); PendingOutputs.Add(StrongOutputPtr); } return StrongOutputPtr; } void FPatchSplitter::AddNewPatch(FPatchOutputStrongPtr&& InPatchOutputStrongPtr) { FScopeLock ScopeLock(&PendingOutputsCriticalSection); PendingOutputs.Add(MoveTemp(InPatchOutputStrongPtr)); } void FPatchSplitter::AddNewPatch(const FPatchOutputStrongPtr& InPatchOutputStrongPtr) { FScopeLock ScopeLock(&PendingOutputsCriticalSection); PendingOutputs.Add(InPatchOutputStrongPtr); } int32 FPatchSplitter::Num() { FScopeLock ScopeLock(&ConnectedOutputsCriticalSection); AddPendingPatches(); return ConnectedOutputs.Num(); } int32 FPatchSplitter::MaxNumberOfSamplesThatCanBePushed() { FScopeLock ScopeLock(&ConnectedOutputsCriticalSection); AddPendingPatches(); if (ConnectedOutputs.IsEmpty()) { return INDEX_NONE; } // Iterate over our outputs and get the smallest remainder of all of our circular buffers. uint32 SmallestRemainder = TNumericLimits::Max(); for (FPatchInput& Input : ConnectedOutputs) { const uint32 InputRemainder = Input.OutputHandle->InternalBuffer.Remainder(); if (InputRemainder > 0 || Input.IsOutputStillActive()) { SmallestRemainder = FMath::Min(SmallestRemainder, InputRemainder); } } return SmallestRemainder; } void FPatchSplitter::AddPendingPatches() { FScopeLock ScopeLock(&PendingOutputsCriticalSection); ConnectedOutputs.Append(MoveTemp(PendingOutputs)); } int32 FPatchSplitter::PushAudio(const float* InBuffer, int32 InNumSamples) { FScopeLock ScopeLock(&ConnectedOutputsCriticalSection); AddPendingPatches(); if (ConnectedOutputs.IsEmpty()) { return INDEX_NONE; } int32 MinimumSamplesPushed = TNumericLimits::Max(); for (int32 Index = ConnectedOutputs.Num() - 1; Index >= 0; Index--) { FPatchInput& ConnectedOutput = ConnectedOutputs[Index]; const int32 NumSamplesPushed = ConnectedOutput.PushAudio(InBuffer, InNumSamples); if (NumSamplesPushed == INDEX_NONE) { ConnectedOutputs.RemoveAtSwap(Index, EAllowShrinking::No); } else { MinimumSamplesPushed = FMath::Min(MinimumSamplesPushed, NumSamplesPushed); } } return MinimumSamplesPushed; } FPatchOutputStrongPtr FPatchMixerSplitter::AddNewOutput(int32 MaxLatencyInSamples, float InGain) { return Splitter.AddNewPatch(MaxLatencyInSamples, InGain); } void FPatchMixerSplitter::AddNewOutput(const FPatchOutputStrongPtr& InPatchOutputStrongPtr) { Splitter.AddNewPatch(InPatchOutputStrongPtr); } FPatchInput FPatchMixerSplitter::AddNewInput(int32 MaxLatencyInSamples, float InGain) { return Mixer.AddNewInput(MaxLatencyInSamples, InGain); } void FPatchMixerSplitter::AddNewInput(FPatchInput& InInput) { Mixer.AddNewInput(InInput); } void FPatchMixerSplitter::RemovePatch(const FPatchInput& InInput) { Mixer.RemovePatch(InInput); } void FPatchMixerSplitter::ProcessAudio() { const int32 NumSamplesToPop = Mixer.MaxNumberOfSamplesThatCanBePopped(); const int32 NumSamplesToPush = Splitter.MaxNumberOfSamplesThatCanBePushed(); const int32 NumSamplesToForward = FMath::Min(NumSamplesToPush, NumSamplesToPop); if (NumSamplesToForward <= 0) { // Early exit when there are either no inputs or no outputs // connected, or one of the inputs has not pushed any audio yet. return; } IntermediateBuffer.Reset(); IntermediateBuffer.AddUninitialized(NumSamplesToForward); // Mix down inputs int32 PopResult = Mixer.PopAudio(IntermediateBuffer.GetData(), NumSamplesToForward, false); check(PopResult == NumSamplesToForward); // Push audio to outputs Splitter.PushAudio(IntermediateBuffer.GetData(), NumSamplesToForward); } }