// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/RuntimeResampler.h" #include "DSP/BufferVectorOperations.h" #include "DSP/Dsp.h" #include "DSP/MultichannelBuffer.h" #include "HAL/PlatformMath.h" #include "Math/UnrealMathUtility.h" // Throughout this file, all variables with the FP suffix are in the 16.16 fixed point format. namespace Audio { const float FRuntimeResampler::MaxFrameRatio = 100.f; const float FRuntimeResampler::MinFrameRatio = 0.01f; const int32 FRuntimeResampler::FPScale = 65536; const float FRuntimeResampler::FPScaleFloat = 65536; FRuntimeResampler::FRuntimeResampler(int32 InNumChannels) { Reset(InNumChannels); } void FRuntimeResampler::Reset(const int32 InNumChannels) { check(InNumChannels > 0); CurrentInputFrameIndexFP = 0; CurrentFrameRatioFP = FPScale; TargetFrameRatioFP = FPScale; FrameRatioFrameDeltaFP = 0; NumFramesToInterpolate = 0; PreviousFrame.Empty(); PreviousFrame.AddZeroed(InNumChannels); TempInputPointers.Empty(); TempInputPointers.AddZeroed(InNumChannels); TempOutputPointers.Empty(); TempOutputPointers.AddZeroed(InNumChannels); } void FRuntimeResampler::SetFrameRatio(float InRatio, int32 InDesiredNumFramesToInterpolate) { if (ensureMsgf((InRatio >= MinFrameRatio) && (InRatio <= MaxFrameRatio), TEXT("The frame ratio (%f) must be between %f and %f."), InRatio, MinFrameRatio, MaxFrameRatio)) { const uint32 RatioFP = FMath::RoundToInt32(InRatio * FPScale); if (InDesiredNumFramesToInterpolate <= 0 || RatioFP == CurrentFrameRatioFP) { // Set frame ratio immediately. CurrentFrameRatioFP = RatioFP; TargetFrameRatioFP = RatioFP; FrameRatioFrameDeltaFP = 0; NumFramesToInterpolate = 0; } else { // Interpolate frame ratio over output frames. TargetFrameRatioFP = RatioFP; const uint32 RatioChangeAbsolute = uint32(FMath::Abs(int32(TargetFrameRatioFP - CurrentFrameRatioFP))); // Round up the frame delta so it's always at least 1 const uint32 FrameDeltaAbsolute = FMath::DivideAndRoundUp(RatioChangeAbsolute, InDesiredNumFramesToInterpolate); // Fudge the number of frames over which we do the interpolation to make sure we land close to the target check(FrameDeltaAbsolute > 0); NumFramesToInterpolate = FMath::DivideAndRoundDown(RatioChangeAbsolute, FrameDeltaAbsolute); if (NumFramesToInterpolate > 0) { FrameRatioFrameDeltaFP = int32(FrameDeltaAbsolute); if (TargetFrameRatioFP < CurrentFrameRatioFP) { FrameRatioFrameDeltaFP = -FrameRatioFrameDeltaFP; } } else { CurrentFrameRatioFP = RatioFP; TargetFrameRatioFP = RatioFP; FrameRatioFrameDeltaFP = 0; check(NumFramesToInterpolate == 0); } } } } int64 FRuntimeResampler::MapOutputFrameToInputFrameFP(const int32 InOutputFrameIndex) const { checkf(InOutputFrameIndex >= 0, TEXT("Frame index mapping function is only value for outputs frames greater than or equal to 0")); int64 InputFrameIndex = 0; if (NumFramesToInterpolate > 0) { if (InOutputFrameIndex < NumFramesToInterpolate) { // Frame ratio interpolation is still occurring at the output frame index. const int64 PreviousIndex = InOutputFrameIndex; const int64 AccumulationOfFrameDeltasFP = FrameRatioFrameDeltaFP * ((PreviousIndex * (PreviousIndex + 1)) / 2); InputFrameIndex = PreviousIndex * CurrentFrameRatioFP + AccumulationOfFrameDeltasFP; } else { // Frame ratio interpolation occurred, but has reached the target frame ratio by the output frame index. const int64 AccumulationOfFrameDeltasFP = FrameRatioFrameDeltaFP * (int64(NumFramesToInterpolate) * (NumFramesToInterpolate + 1) / 2); InputFrameIndex = int64(NumFramesToInterpolate) * CurrentFrameRatioFP + AccumulationOfFrameDeltasFP + int64(InOutputFrameIndex - NumFramesToInterpolate) * TargetFrameRatioFP; } } else { // No interpolation is happening. The math is quite a bit simpler. InputFrameIndex = int64(TargetFrameRatioFP) * InOutputFrameIndex; } // Apply current internal offset. InputFrameIndex += CurrentInputFrameIndexFP; return InputFrameIndex; } int32 FRuntimeResampler::GetNumInputFramesNeededToProduceOutputFrames(const int32 InNumOutputFrames) const { check(InNumOutputFrames >= 0); if (InNumOutputFrames > 0) { const int64 InputFrameFP = MapOutputFrameToInputFrameFP(InNumOutputFrames - 1); // We need the sample after InputFrameFP, so we take the floor and add 1. Because we're returning // the number of samples needed rather than the index of the last sample, we add another 1. return int32(InputFrameFP / FPScale) + 2; } return 0; } int32 FRuntimeResampler::GetNumOutputFramesProducedByInputFrames(const int32 InNumInputFrames) const { check(InNumInputFrames >= 0); if (InNumInputFrames <= 0) { return 0; } const int64 InputFrameIndexFP = int64(InNumInputFrames - 1) * FPScale; const int64 RelativeInputFrameIndexFP = InputFrameIndexFP - CurrentInputFrameIndexFP; int32 OutputFrameIndex; if (RelativeInputFrameIndexFP <= 0) { // We don't have enough input to produce output frame 0. OutputFrameIndex = -1; } else if (NumFramesToInterpolate > 0) { const int64 InputSampleAtBreakpointFP = MapOutputFrameToInputFrameFP(NumFramesToInterpolate); if (InputFrameIndexFP > InputSampleAtBreakpointFP) { OutputFrameIndex = NumFramesToInterpolate + (InputFrameIndexFP - InputSampleAtBreakpointFP - 1) / TargetFrameRatioFP; } else { // Do a binary search for the previous output frame int32 LowerFrameIndex = 0; int32 UpperFrameIndex = NumFramesToInterpolate; while (UpperFrameIndex > LowerFrameIndex + 1) { const int32 MidFrameIndex = (LowerFrameIndex + UpperFrameIndex) / 2; const int64 InputAtMidOutputFP = MapOutputFrameToInputFrameFP(MidFrameIndex); if (InputAtMidOutputFP < InputFrameIndexFP) { LowerFrameIndex = MidFrameIndex; } else { UpperFrameIndex = MidFrameIndex; } } OutputFrameIndex = LowerFrameIndex; } } else { OutputFrameIndex = int32((RelativeInputFrameIndexFP - 1) / TargetFrameRatioFP); } return OutputFrameIndex + 1; } template int32 FRuntimeResampler::ProcessCircularBufferGeneric(FMultichannelCircularBuffer& InAudio, OutputBufferType& OutAudio) { const int32 NumChannels = PreviousFrame.Num(); check(InAudio.Num() == NumChannels && OutAudio.Num() == NumChannels); ResamplingParameters Parameters; Parameters.InputAudio = TempInputPointers; Parameters.InputStride = 1; Parameters.OutputAudio = TempOutputPointers; Parameters.OutputStride = 1; int32 NumFramesConsumed = 0; int32 NumFramesProduced = 0; // The input can be in two disjoint segments, so we run the resampling core up to two times. for (int32 SegmentIdx = 0; SegmentIdx < 2; ++SegmentIdx) { for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { // Set up input pointers DisjointedArrayView InputView = InAudio[ChannelIdx].PeekInPlace(InAudio[ChannelIdx].Num()); if (ChannelIdx == 0) { Parameters.NumInputFrames = InputView.FirstBuffer.Num(); } else { // We require that all of the circular buffers have the same layout check(Parameters.NumInputFrames == InputView.FirstBuffer.Num()); } TempInputPointers[ChannelIdx] = InputView.FirstBuffer.GetData(); // Set up the output pointers OutputChannelType& OutputChannel = OutAudio[ChannelIdx]; TempOutputPointers[ChannelIdx] = OutputChannel.GetData() + NumFramesProduced; if (ChannelIdx == 0) { Parameters.NumOutputFrames = OutputChannel.Num() - NumFramesProduced; } else { check(Parameters.NumOutputFrames == OutputChannel.Num() - NumFramesProduced); // Every output channel must have the same size } } // Run the resampler, pop data from the input ProcessAudioInternal(Parameters); NumFramesConsumed += Parameters.OutNumInputFramesConsumed; NumFramesProduced += Parameters.OutNumOutputFramesProduced; for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { InAudio[ChannelIdx].Pop(Parameters.OutNumInputFramesConsumed); } // Skip the second pass if there's nothing left to do if (NumFramesProduced == OutAudio[0].Num() || InAudio[0].Num() == 0) { break; } } return NumFramesProduced; } int32 FRuntimeResampler::ProcessCircularBuffer(FMultichannelCircularBuffer& InAudio, FMultichannelBuffer& OutAudio) { return ProcessCircularBufferGeneric(InAudio, OutAudio); } int32 FRuntimeResampler::ProcessCircularBuffer(FMultichannelCircularBuffer& InAudio, FMultichannelBufferView& OutAudio) { return ProcessCircularBufferGeneric>(InAudio, OutAudio); } void FRuntimeResampler::ProcessInterleaved(const TArrayView Input, const TArrayView Output, int32& OutNumInputFramesConsumed, int32& OutNumOutputFramesProduced) { const int32 NumChannels = PreviousFrame.Num(); for (int32 Channel = 0; Channel < NumChannels; ++Channel) { TempInputPointers[Channel] = Input.GetData() + Channel; TempOutputPointers[Channel] = Output.GetData() + Channel; } ResamplingParameters Params{}; Params.InputAudio = TempInputPointers; Params.NumInputFrames = Input.Num() / NumChannels; Params.InputStride = NumChannels; Params.OutputAudio = TempOutputPointers; Params.NumOutputFrames = Output.Num() / NumChannels; Params.OutputStride = NumChannels; ProcessAudioInternal(Params); OutNumInputFramesConsumed = Params.OutNumInputFramesConsumed; OutNumOutputFramesProduced = Params.OutNumOutputFramesProduced; } void FRuntimeResampler::ProcessAudioInternal(ResamplingParameters& Parameters) { check(Parameters.InputStride > 0 && Parameters.NumInputFrames >= 0 && Parameters.OutputStride > 0 && Parameters.NumOutputFrames >= 0); const int32 NumChannels = PreviousFrame.Num(); check(NumChannels > 0 && NumChannels == Parameters.InputAudio.Num() && NumChannels == Parameters.OutputAudio.Num()); // Prevent overflowing an int32 in our fixed-point input index by dividing into batches const int32 MaxSamplesPerRun = 30000; if (Parameters.NumInputFrames > MaxSamplesPerRun) { ProcessAudioInBatches(Parameters, MaxSamplesPerRun); return; } // This will be reduced later if we don't have enough input. int32 NumOutputFrames = Parameters.NumOutputFrames; if (NumOutputFrames < 1 || Parameters.NumInputFrames < 1) { Parameters.OutNumInputFramesConsumed = 0; Parameters.OutNumOutputFramesProduced = 0; return; } float const* const* const InputPointers = Parameters.InputAudio.GetData(); float* const* const OutputPointers = Parameters.OutputAudio.GetData(); uint32 InputFrameRatioFP = CurrentFrameRatioFP; int32 InputFrameIndexFP = CurrentInputFrameIndexFP; const int32 OutputStride = Parameters.OutputStride; const int32 InputStride = Parameters.InputStride; // See if we can quickly do Memcpy's instead of resampling if (CurrentInputFrameIndexFP == 0 && TargetFrameRatioFP == FPScale && NumFramesToInterpolate == 0) { if (DoDirectCopy(Parameters)) { return; } } // Check that InputFrameIndexFP is >= -1 checkf(InputFrameIndexFP >= -FPScale, TEXT("Input frame index references discarded data")); // Figure out how many samples we can safely generate in the core loop const int32 NumFramesNeeded = GetNumInputFramesNeededToProduceOutputFrames(NumOutputFrames); if (NumFramesNeeded > Parameters.NumInputFrames) { NumOutputFrames = GetNumOutputFramesProducedByInputFrames(Parameters.NumInputFrames); } const int32 FinalNumFramesToInterpolate = FMath::Max(NumFramesToInterpolate - NumOutputFrames, 0); const int32 OutputEndIndex = NumOutputFrames * Parameters.OutputStride; int32 OutputSampleIndex = 0; // Output any samples that involve PreviousFrame while (InputFrameIndexFP < 0 && OutputSampleIndex < OutputEndIndex) { const float Alpha = (InputFrameIndexFP + FPScale) / FPScaleFloat; for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { OutputPointers[ChannelIdx][OutputSampleIndex] = FMath::Lerp(PreviousFrame[ChannelIdx], InputPointers[ChannelIdx][0], Alpha); } OutputSampleIndex += OutputStride; if (NumFramesToInterpolate > 0) { if (--NumFramesToInterpolate == 0) { InputFrameRatioFP = TargetFrameRatioFP; FrameRatioFrameDeltaFP = 0; } else { InputFrameRatioFP += FrameRatioFrameDeltaFP; } } InputFrameIndexFP += InputFrameRatioFP; } // Do the bulk of the processing in an optimized core if (NumChannels == 1 && InputStride == 1 && OutputStride == 1) { MonoResamplingCore(InputPointers[0], OutputPointers[0], InputFrameIndexFP, InputFrameRatioFP, OutputSampleIndex, OutputEndIndex); } else if (NumChannels == 2 && InputStride == 2 && OutputStride == 2) { StereoInterleavedResamplingCore(InputPointers[0], OutputPointers[0], InputFrameIndexFP, InputFrameRatioFP, OutputSampleIndex, OutputEndIndex); } else if (NumChannels == 2 && InputStride == 1 && OutputStride == 1) { StereoDeinterleavedResamplingCore(InputPointers[0], InputPointers[1], OutputPointers[0], OutputPointers[1], InputFrameIndexFP, InputFrameRatioFP, OutputSampleIndex, OutputEndIndex); } else { GenericResamplingCore(Parameters, InputFrameIndexFP, InputFrameRatioFP, OutputSampleIndex, OutputEndIndex); } NumFramesToInterpolate = FinalNumFramesToInterpolate; // Set output values const uint32 NumInputFramesConsumed = InputFrameIndexFP < 0 ? 0 : FMath::Min(InputFrameIndexFP / FPScale + 1, Parameters.NumInputFrames); Parameters.OutNumInputFramesConsumed = NumInputFramesConsumed; Parameters.OutNumOutputFramesProduced = NumOutputFrames; // Make sure we don't fail to make any progress check(NumOutputFrames > 0 || NumInputFramesConsumed > 0); // Update saved state for next run if (NumInputFramesConsumed > 0) { InputFrameIndexFP -= NumInputFramesConsumed * FPScale; // Save final consumed input frame if we'll need it next run if (InputFrameIndexFP < 0) { const int32 SampleIndexToSave = (NumInputFramesConsumed - 1) * InputStride; for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { PreviousFrame[ChannelIdx] = InputPointers[ChannelIdx][SampleIndexToSave]; } } } if (NumFramesToInterpolate <= 0) { check(NumFramesToInterpolate == 0); CurrentFrameRatioFP = TargetFrameRatioFP; FrameRatioFrameDeltaFP = 0; } else { CurrentFrameRatioFP = InputFrameRatioFP; } CurrentInputFrameIndexFP = InputFrameIndexFP; } void FRuntimeResampler::ProcessAudioInBatches(ResamplingParameters& Parameters, const int32 MaxSamplesPerBatch) { const int32 NumChannels = PreviousFrame.Num(); int32 InputFramesRemaining = Parameters.NumInputFrames; Parameters.OutNumInputFramesConsumed = 0; Parameters.OutNumOutputFramesProduced = 0; // Create temporary copies of the pointer arrrays and resampling parameters ResamplingParameters BatchParameters(Parameters); TArray> BatchInputAudio(Parameters.InputAudio); TArray> BatchOutputAudio(Parameters.OutputAudio); BatchParameters.InputAudio = BatchInputAudio; BatchParameters.OutputAudio = BatchOutputAudio; while (InputFramesRemaining > 0 && BatchParameters.NumOutputFrames > 0) { // Process a batch BatchParameters.NumInputFrames = FMath::Min(MaxSamplesPerBatch, InputFramesRemaining); ProcessAudioInternal(BatchParameters); // Reduce frame counts and advance pointers for the next batch Parameters.OutNumInputFramesConsumed += BatchParameters.OutNumInputFramesConsumed; Parameters.OutNumOutputFramesProduced += BatchParameters.OutNumOutputFramesProduced; InputFramesRemaining -= BatchParameters.OutNumInputFramesConsumed; BatchParameters.NumOutputFrames -= BatchParameters.OutNumOutputFramesProduced; for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { BatchInputAudio[ChannelIdx] += BatchParameters.OutNumInputFramesConsumed * Parameters.InputStride; BatchOutputAudio[ChannelIdx] += BatchParameters.OutNumOutputFramesProduced * Parameters.OutputStride; } } } bool FRuntimeResampler::DoDirectCopy(ResamplingParameters& Parameters) { const int32 NumChannels = PreviousFrame.Num(); float const* const* const InputPointers = Parameters.InputAudio.GetData(); float* const* const OutputPointers = Parameters.OutputAudio.GetData(); if (Parameters.InputStride == 1 && Parameters.OutputStride == 1) { // Copy multi-mono buffers const int32 CopyFrames = FMath::Min(Parameters.NumInputFrames, Parameters.NumOutputFrames); const int32 CopyBytes = CopyFrames * sizeof(float); for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { FMemory::Memcpy(OutputPointers[ChannelIdx], InputPointers[ChannelIdx], CopyBytes); } Parameters.OutNumInputFramesConsumed = CopyFrames; Parameters.OutNumOutputFramesProduced = CopyFrames; return true; } else if (Parameters.InputStride == NumChannels && Parameters.OutputStride == NumChannels) { // Check if this is a complete interleaved buffer for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { if (InputPointers[ChannelIdx] != InputPointers[0] + ChannelIdx || OutputPointers[ChannelIdx] != OutputPointers[0] + ChannelIdx) { return false; // Not appropriately interleaved. } } // Do one big copy const int32 CopyFrames = FMath::Min(Parameters.NumInputFrames, Parameters.NumOutputFrames); const int32 CopyBytes = CopyFrames * NumChannels * sizeof(float); FMemory::Memcpy(OutputPointers[0], InputPointers[0], CopyBytes); Parameters.OutNumInputFramesConsumed = CopyFrames; Parameters.OutNumOutputFramesProduced = CopyFrames; return true; } return false; } void FRuntimeResampler::GenericResamplingCore(const ResamplingParameters& Parameters, int32& OutInputFrameIndexFP, uint32& OutInputFrameRatioFP, int32 OutputSampleIndex, const int32 OutputEndIndex) { const int32 NumChannels = PreviousFrame.Num(); const int32 OutputStride = Parameters.OutputStride; const int32 InputStride = Parameters.InputStride; float const* const* const InputPointers = Parameters.InputAudio.GetData(); float* const* const OutputPointers = Parameters.OutputAudio.GetData(); int32 InputFrameIndexFP = OutInputFrameIndexFP; int32 InputFrameRatioFP = OutInputFrameRatioFP; // Handle frames where samples are interpolated. const int32 BatchEndIndex = FMath::Min(OutputEndIndex, OutputSampleIndex + NumFramesToInterpolate * OutputStride); for (; OutputSampleIndex < BatchEndIndex; OutputSampleIndex += OutputStride) { const int32 LowerSampleIndex = (InputFrameIndexFP / FPScale) * InputStride; const int32 UpperSampleIndex = LowerSampleIndex + InputStride; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; checkSlow(UpperSampleIndex < Parameters.NumInputFrames * InputStride); for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { OutputPointers[ChannelIdx][OutputSampleIndex] = FMath::Lerp(InputPointers[ChannelIdx][LowerSampleIndex], InputPointers[ChannelIdx][UpperSampleIndex], Alpha); } InputFrameRatioFP += FrameRatioFrameDeltaFP; InputFrameIndexFP += InputFrameRatioFP; } // Handle frames where sample rate is constant. for (; OutputSampleIndex < OutputEndIndex; OutputSampleIndex += OutputStride) { const int32 LowerSampleIndex = (InputFrameIndexFP / FPScale) * InputStride; const int32 UpperSampleIndex = LowerSampleIndex + InputStride; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; checkSlow(UpperSampleIndex < Parameters.NumInputFrames * InputStride); for (int32 ChannelIdx = 0; ChannelIdx < NumChannels; ++ChannelIdx) { OutputPointers[ChannelIdx][OutputSampleIndex] = FMath::Lerp(InputPointers[ChannelIdx][LowerSampleIndex], InputPointers[ChannelIdx][UpperSampleIndex], Alpha); } InputFrameIndexFP += TargetFrameRatioFP; } OutInputFrameIndexFP = InputFrameIndexFP; OutInputFrameRatioFP = InputFrameRatioFP; } void FRuntimeResampler::MonoResamplingCore(const float* Input, float* Output, int32& OutInputFrameIndexFP, uint32& OutInputFrameRatioFP, int32 OutputSampleIndex, const int32 OutputEndIndex) { int32 InputFrameIndexFP = OutInputFrameIndexFP; uint32 InputFrameRatioFP = OutInputFrameRatioFP; // Handle frames where samples are interpolated. const int32 BatchEndIndex = FMath::Min(OutputEndIndex, OutputSampleIndex + NumFramesToInterpolate); for (; OutputSampleIndex < BatchEndIndex; OutputSampleIndex++) { const int32 LowerSampleIndex = InputFrameIndexFP / FPScale; const int32 UpperSampleIndex = LowerSampleIndex + 1; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; Output[OutputSampleIndex] = FMath::Lerp(Input[LowerSampleIndex], Input[UpperSampleIndex], Alpha); InputFrameRatioFP += FrameRatioFrameDeltaFP; InputFrameIndexFP += InputFrameRatioFP; } // Handle frames where sample rate is constant. for (; OutputSampleIndex < OutputEndIndex; OutputSampleIndex++) { const int32 LowerSampleIndex = InputFrameIndexFP / FPScale; const int32 UpperSampleIndex = LowerSampleIndex + 1; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; Output[OutputSampleIndex] = FMath::Lerp(Input[LowerSampleIndex], Input[UpperSampleIndex], Alpha); InputFrameIndexFP += TargetFrameRatioFP; } OutInputFrameIndexFP = InputFrameIndexFP; OutInputFrameRatioFP = InputFrameRatioFP; } void FRuntimeResampler::StereoInterleavedResamplingCore(const float* Input, float* Output, int32& OutInputFrameIndexFP, uint32& OutInputFrameRatioFP, int32 OutputSampleIndex, const int32 OutputEndIndex) { int32 InputFrameIndexFP = OutInputFrameIndexFP; int32 InputFrameRatioFP = OutInputFrameRatioFP; // Handle frames where samples are interpolated. const int32 BatchEndIndex = FMath::Min(OutputEndIndex, OutputSampleIndex + NumFramesToInterpolate * 2); for (; OutputSampleIndex < BatchEndIndex; OutputSampleIndex += 2) { const int32 LowerSampleIndex = (InputFrameIndexFP / FPScale) * 2; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; Output[OutputSampleIndex] = FMath::Lerp(Input[LowerSampleIndex], Input[LowerSampleIndex + 2], Alpha); Output[OutputSampleIndex + 1] = FMath::Lerp(Input[LowerSampleIndex + 1], Input[LowerSampleIndex + 3], Alpha); InputFrameRatioFP += FrameRatioFrameDeltaFP; InputFrameIndexFP += InputFrameRatioFP; } // Handle frames where sample rate is constant. for (; OutputSampleIndex < OutputEndIndex; OutputSampleIndex += 2) { const int32 LowerSampleIndex = (InputFrameIndexFP / FPScale) * 2; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; Output[OutputSampleIndex] = FMath::Lerp(Input[LowerSampleIndex], Input[LowerSampleIndex + 2], Alpha); Output[OutputSampleIndex + 1] = FMath::Lerp(Input[LowerSampleIndex + 1], Input[LowerSampleIndex + 3], Alpha); InputFrameIndexFP += TargetFrameRatioFP; } OutInputFrameIndexFP = InputFrameIndexFP; OutInputFrameRatioFP = InputFrameRatioFP; } void FRuntimeResampler::StereoDeinterleavedResamplingCore(const float* InputLeft, const float* InputRight, float* OutputLeft, float* OutputRight, int32& OutInputFrameIndexFP, uint32& OutInputFrameRatioFP, int32 OutputSampleIndex, const int32 OutputEndIndex) { int32 InputFrameIndexFP = OutInputFrameIndexFP; int32 InputFrameRatioFP = OutInputFrameRatioFP; // Handle frames where samples are interpolated. const int32 BatchEndIndex = FMath::Min(OutputEndIndex, OutputSampleIndex + NumFramesToInterpolate); for (; OutputSampleIndex < BatchEndIndex; OutputSampleIndex++) { const int32 LowerSampleIndex = InputFrameIndexFP / FPScale; const int32 UpperSampleIndex = LowerSampleIndex + 1; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; OutputLeft[OutputSampleIndex] = FMath::Lerp(InputLeft[LowerSampleIndex], InputLeft[UpperSampleIndex], Alpha); OutputRight[OutputSampleIndex] = FMath::Lerp(InputRight[LowerSampleIndex], InputRight[UpperSampleIndex], Alpha); InputFrameRatioFP += FrameRatioFrameDeltaFP; InputFrameIndexFP += InputFrameRatioFP; } // Handle frames where sample rate is constant. for (; OutputSampleIndex < OutputEndIndex; OutputSampleIndex++) { const int32 LowerSampleIndex = InputFrameIndexFP / FPScale; const int32 UpperSampleIndex = LowerSampleIndex + 1; const float Alpha = (InputFrameIndexFP & (FPScale - 1)) / FPScaleFloat; OutputLeft[OutputSampleIndex] = FMath::Lerp(InputLeft[LowerSampleIndex], InputLeft[UpperSampleIndex], Alpha); OutputRight[OutputSampleIndex] = FMath::Lerp(InputRight[LowerSampleIndex], InputRight[UpperSampleIndex], Alpha); InputFrameIndexFP += TargetFrameRatioFP; } OutInputFrameIndexFP = InputFrameIndexFP; OutInputFrameRatioFP = InputFrameRatioFP; } }