// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/AudioChannelFormatConverter.h" #include "CoreMinimal.h" #include "DSP/BufferVectorOperations.h" #include "DSP/FloatArrayMath.h" #include "SignalProcessingModule.h" namespace Audio { FBaseChannelFormatConverter::~FBaseChannelFormatConverter() = default; const IChannelFormatConverter::FInputFormat& FBaseChannelFormatConverter::GetInputFormat() const { return InputFormat; } const IChannelFormatConverter::FOutputFormat& FBaseChannelFormatConverter::GetOutputFormat() const { return OutputFormat; } void FBaseChannelFormatConverter::SetOutputGain(float InOutputGain, bool bFadeToGain) { OutputGainState.bFadeToNextGain = bFadeToGain; if (bFadeToGain) { // If fading, set as the next gain to fade to. OutputGainState.NextGain = InOutputGain; } else { OutputGainState.Gain = InOutputGain; } } void FBaseChannelFormatConverter::SetMixGain(const FChannelMixEntry& InEntry, bool bFadeToGain) { SetMixGain(InEntry.InputChannelIndex, InEntry.OutputChannelIndex, InEntry.Gain, bFadeToGain); } void FBaseChannelFormatConverter::SetMixGain(int32 InInputChannelIndex, int32 InOutputChannelIndex, float InGain, bool bFadeToGain) { FChannelMixKey Key(InInputChannelIndex, InOutputChannelIndex); if ((InInputChannelIndex >= InputFormat.NumChannels) || (InInputChannelIndex < 0)) { UE_LOG(LogSignalProcessing, Warning, TEXT("Skipping mix entry. Input channel (%d) does not exist for input format with %d channels."), InInputChannelIndex, InputFormat.NumChannels); return; } if ((InOutputChannelIndex >= OutputFormat.NumChannels) || (InOutputChannelIndex < 0)) { UE_LOG(LogSignalProcessing, Warning, TEXT("Skipping mix entry. Output channel (%d) does not exist for output format with %d channels."), InOutputChannelIndex, OutputFormat.NumChannels); return; } if ((InGain == 0.f) && !bFadeToGain) { // Remove a mix state if it has zero gain in order to avoid extra // processing of gain entries. ChannelMixStates.Remove(Key); } else { FChannelMixState* State = ChannelMixStates.Find(Key); if (nullptr == State) { // No existing gain state for the input/output chnannel pair. // One is created here. FChannelMixState& NewState = ChannelMixStates.Add(Key); NewState.InputChannelIndex = InInputChannelIndex; NewState.OutputChannelIndex = InOutputChannelIndex; NewState.Gain = 0.f; State = &NewState; } check(nullptr != State); State->bFadeToNextGain = bFadeToGain; if (bFadeToGain) { // Setup next gain if fading. State->NextGain = InGain; } else { // Set current gain if not fading. State->Gain = InGain; } } } float FBaseChannelFormatConverter::GetTargetMixGain(int32 InInputChannelIndex, int32 InOutputChannelIndex) const { FChannelMixKey Key(InInputChannelIndex, InOutputChannelIndex); if (ChannelMixStates.Contains(Key)) { const FChannelMixState& State = ChannelMixStates.FindChecked(Key); // Check whether to return next gain or current gain. // "NextGain" is meaningless if bFadeToNextGain == false. return State.bFadeToNextGain ? State.NextGain : State.Gain; } return 0.f; } float FBaseChannelFormatConverter::GetTargetOutputGain() const { // Check whether to return next gain or current gain. // "NextGain" is meaningless if bFadeToNextGain == false. return OutputGainState.bFadeToNextGain ? OutputGainState.NextGain : OutputGainState.Gain; } void FBaseChannelFormatConverter::ProcessAudio(const TArray& InInputBuffers, TArray& OutOutputBuffers) { using FMixElement = TSortedMap::ElementType; check(InInputBuffers.Num() == InputFormat.NumChannels); // Ensure output buffers exist in output array. while (OutOutputBuffers.Num() < OutputFormat.NumChannels) { OutOutputBuffers.Emplace(); } for (int32 i = 0; i < OutputFormat.NumChannels; i++) { // Allocate output buffers to the correct size. OutOutputBuffers[i].Reset(); OutOutputBuffers[i].AddUninitialized(NumFramesPerCall); // Zero out data in output buffers. FMemory::Memset(OutOutputBuffers[i].GetData(), 0, sizeof(float) * NumFramesPerCall); } // Cache current input buffer information to minimize duplicate processing // and improve cache performance. MixEntries are sorted by input buffer. // All mix entries for a single input index will be processed sequentially // before the next input index is processed. The `CurrentInputChannelIndex` // is used to determine whether the input channel in a mix entry differs // from the previous. If so, the "Current" input channel is updated. int32 CurrentInputChannelIndex = INDEX_NONE; const FAlignedFloatBuffer* CurrentInputChannel = nullptr; TArray EntriesToRemove; // Process all mix entries. for (FMixElement& Element : ChannelMixStates) { FChannelMixState& MixState = Element.Value; // Check if the input index differs from the previous. If so, update // "Current" input index. if (MixState.InputChannelIndex != CurrentInputChannelIndex) { // Check validity of input channel. const FAlignedFloatBuffer& InputBuffer = InInputBuffers[MixState.InputChannelIndex]; if (ensure(InputBuffer.Num() == NumFramesPerCall)) { CurrentInputChannelIndex = MixState.InputChannelIndex; CurrentInputChannel = &InputBuffer; } else { UE_LOG(LogSignalProcessing, Warning, TEXT("Input buffer frame count (%d) does not match expected frame count (%d)"), InputBuffer.Num(), NumFramesPerCall); CurrentInputChannelIndex = INDEX_NONE; CurrentInputChannel = nullptr; continue; } } check(nullptr != CurrentInputChannel); // Get gain values for mix entry. Include the output gain. const float InitialGain = MixState.Gain * OutputGainState.Gain; const float FinalMixGain = MixState.bFadeToNextGain ? MixState.NextGain : MixState.Gain; const float FinalOutputGain = OutputGainState.bFadeToNextGain ? OutputGainState.NextGain : OutputGainState.Gain; const float FinalGain = FinalMixGain * FinalOutputGain; if (FMath::IsNearlyEqual(InitialGain, FinalGain, 0.000001f)) { // No fade is needed because gain is constant. ArrayMultiplyAddInPlace(*CurrentInputChannel, FinalGain, OutOutputBuffers[MixState.OutputChannelIndex]); } else { // Fade required because gain changes. ArrayLerpAddInPlace(*CurrentInputChannel, InitialGain, FinalGain, OutOutputBuffers[MixState.OutputChannelIndex]); } if (MixState.bFadeToNextGain) { // Update gain state if fade has been processed. MixState.bFadeToNextGain = false; MixState.Gain = MixState.NextGain; if (0.f == MixState.Gain) { // Remove mix states with zero gain. EntriesToRemove.Emplace(MixState); } } } if (OutputGainState.bFadeToNextGain) { // Update gain state if fade has been processed. OutputGainState.Gain = OutputGainState.NextGain; OutputGainState.bFadeToNextGain = false; } for (const FChannelMixKey& Key : EntriesToRemove) { // Remove mix states with zero gain. ChannelMixStates.Remove(Key); } } TUniquePtr FBaseChannelFormatConverter::CreateBaseFormatConverter(const FInputFormat& InInputFormat, const FOutputFormat& InOutputFormat, TArrayView InMixEntries, int32 InNumFramesPerCall) { if (InInputFormat.NumChannels < 1) { UE_LOG(LogSignalProcessing, Error, TEXT("Invalid input format channel count (%d). Must be greater than zero"), InInputFormat.NumChannels); return TUniquePtr(nullptr); } if (InOutputFormat.NumChannels < 1) { UE_LOG(LogSignalProcessing, Error, TEXT("Invalid output format channel count (%d). Must be greater than zero"), InOutputFormat.NumChannels); return TUniquePtr(nullptr); } if (InNumFramesPerCall < 1) { UE_LOG(LogSignalProcessing, Error, TEXT("Invalid num frames per call (%d). Must be greater than zero"), InNumFramesPerCall); return TUniquePtr(nullptr); } return TUniquePtr(new FBaseChannelFormatConverter(InInputFormat, InOutputFormat, InMixEntries, InNumFramesPerCall)); } FBaseChannelFormatConverter::FBaseChannelFormatConverter(const FInputFormat& InInputFormat, const FOutputFormat& InOutputFormat, TArrayView InMixEntries, int32 InNumFramesPerCall) : InputFormat(InInputFormat) , OutputFormat(InOutputFormat) , NumFramesPerCall(InNumFramesPerCall) { check(InputFormat.NumChannels > 0); check(OutputFormat.NumChannels > 0); check(InNumFramesPerCall > 0); const bool bFadeToGain = false; for (const FChannelMixEntry& Entry : InMixEntries) { SetMixGain(Entry, bFadeToGain); } } }