// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "Containers/Array.h" #include "DSP/AlignedBuffer.h" #include "HAL/Platform.h" #include "HAL/UnrealMemory.h" #include "Logging/LogMacros.h" #include "Math/UnrealMath.h" #include "Misc/ScopeLock.h" #include "SignalProcessingModule.h" #include "Templates/IsFloatingPoint.h" #include "Templates/IsIntegral.h" #include "Templates/IsSigned.h" // Macros which can be enabled to cause DSP sample checking #if 0 #define CHECK_SAMPLE(VALUE) #define CHECK_SAMPLE2(VALUE) #else #define CHECK_SAMPLE(VALUE) Audio::CheckSample(VALUE) #define CHECK_SAMPLE2(VALUE) Audio::CheckSample(VALUE) #endif namespace Audio { // Utility to check for sample clipping. Put breakpoint in conditional to find // DSP code that's not behaving correctly inline void CheckSample(float InSample, float Threshold = 0.001f) { if (InSample > Threshold || InSample < -Threshold) { UE_LOG(LogSignalProcessing, Log, TEXT("SampleValue Was %.2f"), InSample); } } // Clamps floats to 0 if they are in sub-normal range UE_DEPRECATED(5.5, "Audio code relies on denormals flush to zero floating point mode from now on. Please use FScopedFTZFloatMode instead of this API") FORCEINLINE float UnderflowClamp(const float InValue) { if (InValue > -FLT_MIN && InValue < FLT_MIN) { return 0.0f; } return InValue; } // Function converts linear scale volume to decibels FORCEINLINE float ConvertToDecibels(const float InLinear, const float InFloor = UE_SMALL_NUMBER) { return 20.0f * FMath::LogX(10.0f, FMath::Max(InLinear, InFloor)); } // Function converts decibel to linear scale FORCEINLINE float ConvertToLinear(const float InDecibels) { return FMath::Pow(10.0f, InDecibels / 20.0f); } // Given a velocity value [0,127], return the linear gain FORCEINLINE float GetGainFromVelocity(const float InVelocity) { if (InVelocity == 0.0f) { return 0.0f; } return (InVelocity * InVelocity) / (127.0f * 127.0f); } // Low precision, high performance approximation of sine using parabolic polynomial approx // Valid on interval [-PI, PI] FORCEINLINE float FastSin(const float X) { return (4.0f * X) / UE_PI * (1.0f - FMath::Abs(X) / UE_PI); } // Slightly higher precision, high performance approximation of sine using parabolic polynomial approx FORCEINLINE float FastSin2(const float X) { float X2 = FastSin(X); X2 = 0.225f * (X2* FMath::Abs(X2) - X2) + X2; return X2; } // Valid on interval [-PI, PI] // Sine approximation using Bhaskara I technique discovered in 7th century. // https://en.wikipedia.org/wiki/Bh%C4%81skara_I FORCEINLINE float FastSin3(const float X) { const float AbsX = FMath::Abs(X); const float Numerator = 16.0f * X * (UE_PI - AbsX); const float Denominator = 5.0f * UE_PI * UE_PI - 4.0f * AbsX * (UE_PI - AbsX); return Numerator / Denominator; } /** * Generates a sine wave in the given buffer with the given frequency. * * Uses the method of generating a 2D point on the unit circle and multiplying * with a 2d rotation matrix to advance the phase. Very accurate and very fast. * * Use this unless you're changing frequency every few samples on very tiny * blocks (single digit sample count) * * Distortion vs directly calling FMath::Sin() is on the order of -100db, and is roughly * 10x faster (for constant frequency). * * Frequency changes are not interpolated across the buffer, so dramatic changes * could introduce aliasing due to a discontinuous derivative in the output. * If you're changing frequencies by a lot, constantly, you should probably * use VectorSinCos in a loop. * * Usage: * * constexpr const int32 ChunkSampleCount = 480; * alignas (sizeof(VectorRegister4Float)) float ChunkBuffer[ChunkSampleCount]; * FSinOscBufferGenerator Generator; * Generator.GenerateBuffer(48000, 400, ChunkBuffer, ChunkSampleCount); * * ChunkBuffer now has 10ms of sine tone at 400hz. The next call will * continue the sine tone where it left off. * */ class FSinOsc2DRotation { public: FSinOsc2DRotation(float InStartingPhaseRadians = 0.f) { LastPhasePerSample = -1; LastPhase = InStartingPhaseRadians; QuadDxVec = VectorZeroFloat(); QuadDyVec = VectorZeroFloat(); } /** * Generates the sine tone, continuing from the last phase. * * @param SampleRate the sample rate the buffer will be played at * @param ClampedFrequency the frequency of the tone to emit, clamped to nyquist (SampleRate/2) * @param Buffer the output buffer * @param BufferSampleCount the number of samples in the buffer. Generally, this should be %4 granularity. * */ void GenerateBuffer(float SampleRate, float ClampedFrequency, float* Buffer, int32 BufferSampleCount) { // Regenerate our vector rotation components if our changes. const float PhasePerSample = (ClampedFrequency * (2 * UE_PI)) / (SampleRate); if (LastPhasePerSample != PhasePerSample) { float QuadDx = FMath::Cos(PhasePerSample * 4); float QuadDy = FMath::Sin(PhasePerSample * 4); QuadDxVec = VectorLoadFloat1(&QuadDx); QuadDyVec = VectorLoadFloat1(&QuadDy); LastPhasePerSample = PhasePerSample; } float* Write = Buffer; // The rotation matrix drifts, so we resync every so often while (BufferSampleCount) { // The concept here is that cos/sin are points on a unit circle, // so an oscilator is just a rotation of that point. // // To avoid drift, we resync off of an accurate sin/cos every evaluation // then do a 2d rotation in the actual loop. // // we store 4 points at once to use SIMD, so each rotation is actually // 4x the phase delta. // alignas(16) float PhaseSource[4]; PhaseSource[0] = LastPhase + 0 * PhasePerSample; PhaseSource[1] = LastPhase + 1 * PhasePerSample; PhaseSource[2] = LastPhase + 2 * PhasePerSample; PhaseSource[3] = LastPhase + 3 * PhasePerSample; VectorRegister4Float PhaseVec = VectorLoad(PhaseSource); VectorRegister4Float XVector, YVector; // We need an accurate representation of the delta // vectors since we are integrating it VectorSinCos(&YVector, &XVector, &PhaseVec); // Copy to local (the compiler actually didn't do this!) VectorRegister4Float LocalDxVec = QuadDxVec; VectorRegister4Float LocalDyVec = QuadDyVec; int32 BlockSampleCount = BufferSampleCount; if (BlockSampleCount > 480) BlockSampleCount = 480; // Unrolling this didn't seem to really help perf wise. int32 Block4 = BlockSampleCount >> 2; while (Block4) { VectorStore(YVector, Write); // 2D rotation matrix. VectorRegister4Float NewX = VectorSubtract(VectorMultiply(LocalDxVec, XVector), VectorMultiply(LocalDyVec, YVector)); VectorRegister4Float NewY = VectorAdd(VectorMultiply(LocalDyVec, XVector), VectorMultiply(LocalDxVec, YVector)); XVector = NewX; YVector = NewY; Write += 4; Block4--; } constexpr int32 SIMD_MASK = 0x00000003; if (BlockSampleCount & SIMD_MASK) { // We've actually already calculated the next quad - it's in YVector alignas(16) float YFloats[4]; VectorStore(YVector, YFloats); int32 Remn = BlockSampleCount & SIMD_MASK; for (int32 i = 0; i < Remn; i++) { Write[i] = YFloats[i]; } } // Advance phase, range reduce, and store. float PhaseInRadians = LastPhase + (float)BlockSampleCount * PhasePerSample; PhaseInRadians -= FMath::FloorToFloat(PhaseInRadians / (2 * UE_PI)) * (2 * UE_PI); LastPhase = PhaseInRadians; BufferSampleCount -= BlockSampleCount; } // end while BufferSampleCount } // end GenerateBuffer private: VectorRegister4Float QuadDxVec, QuadDyVec; float LastPhasePerSample; float LastPhase; }; // end FSinOsc2DRotation // Fast tanh based on pade approximation FORCEINLINE float FastTanh(float X) { if (X < -3) return -1.0f; if (X > 3) return 1.0f; const float InputSquared = X*X; return X*(27.0f + InputSquared) / (27.0f + 9.0f * InputSquared); } // Based on sin parabolic approximation FORCEINLINE float FastTan(float X) { const float Num = X * (1.0f - FMath::Abs(X) / UE_PI); const float Den = (X + 0.5f * UE_PI) * (1.0f - FMath::Abs(X + 0.5f * UE_PI) / UE_PI); return Num / Den; } // Gets polar value from unipolar FORCEINLINE float GetBipolar(const float X) { return 2.0f * X - 1.0f; } // Converts bipolar value to unipolar FORCEINLINE float GetUnipolar(const float X) { return 0.5f * X + 0.5f; } // Converts in place a buffer into Unipolar (0..1) // This is a Vector op version of the GetUnipolar function. (0.5f * X + 0.5f). inline void ConvertBipolarBufferToUnipolar(float* InAlignedBuffer, int32 NumSamples) { // Make sure buffers are aligned and we can do a whole number of loops. check(NumSamples % 4 == 0); const VectorRegister4Float Half = VectorSetFloat1(0.5f); // Process buffer 1 vector (4 floats) at a time. for(int32 i = NumSamples / 4; i; --i, InAlignedBuffer += 4) { VectorRegister4Float V = VectorLoad(InAlignedBuffer); V = VectorMultiply(V, Half); V = VectorAdd(V, Half); VectorStore(V, InAlignedBuffer); } } // Using midi tuning standard, compute frequency in hz from midi value FORCEINLINE float GetFrequencyFromMidi(const float InMidiNote) { return 440.0f * FMath::Pow(2.0f, (InMidiNote - 69.0f) / 12.0f); } // Returns the log frequency of the input value. Maps linear domain and range values to log output (good for linear slider controlling frequency) FORCEINLINE float GetLogFrequencyClamped(const float InValue, const FVector2D& Domain, const FVector2D& Range) { // Check if equal as well as less than to avoid round error in case where at edges. if (InValue <= Domain.X) { return UE_REAL_TO_FLOAT(Range.X); } if (InValue >= Domain.Y) { return UE_REAL_TO_FLOAT(Range.Y); } //Handle edge case of NaN if (FMath::IsNaN(InValue)) { return UE_REAL_TO_FLOAT(Range.X); } const FVector2D RangeLog(FMath::Max(FMath::Loge(Range.X), UE_SMALL_NUMBER), FMath::Min(FMath::Loge(Range.Y), UE_BIG_NUMBER)); const float FreqLinear = (float)FMath::GetMappedRangeValueUnclamped(Domain, RangeLog, (FVector2D::FReal)InValue); return FMath::Exp(FreqLinear); } // Returns the linear frequency of the input value. Maps log domain and range values to linear output (good for linear slider representation/visualization of log frequency). Reverse of GetLogFrequencyClamped. FORCEINLINE float GetLinearFrequencyClamped(const float InFrequencyValue, const FVector2D& Domain, const FVector2D& Range) { // Check if equal as well as less than to avoid round error in case where at edges. if (InFrequencyValue <= Range.X) { return UE_REAL_TO_FLOAT(Domain.X); } if (InFrequencyValue >= Range.Y) { return UE_REAL_TO_FLOAT(Domain.Y); } //Handle edge case of NaN if (FMath::IsNaN(InFrequencyValue)) { return UE_REAL_TO_FLOAT(Domain.X); } const FVector2D RangeLog(FMath::Max(FMath::Loge(Range.X), UE_SMALL_NUMBER), FMath::Min(FMath::Loge(Range.Y), UE_BIG_NUMBER)); const FVector2D::FReal FrequencyLog = FMath::Loge(InFrequencyValue); return UE_REAL_TO_FLOAT(FMath::GetMappedRangeValueUnclamped(RangeLog, Domain, FrequencyLog)); } // Using midi tuning standard, compute midi from frequency in hz FORCEINLINE float GetMidiFromFrequency(const float InFrequency) { return 69.0f + 12.0f * FMath::LogX(2.0f, InFrequency / 440.0f); } // Return a pitch scale factor based on the difference between a base midi note and a target midi note. Useful for samplers. FORCEINLINE float GetPitchScaleFromMIDINote(int32 BaseMidiNote, int32 TargetMidiNote) { const float BaseFrequency = GetFrequencyFromMidi(FMath::Clamp((float)BaseMidiNote, 0.0f, 127.0f)); const float TargetFrequency = 440.0f * FMath::Pow(2.0f, ((float)TargetMidiNote - 69.0f) / 12.0f); const float PitchScale = TargetFrequency / BaseFrequency; return PitchScale; } // Returns the frequency multiplier to scale a base frequency given the input semitones FORCEINLINE float GetFrequencyMultiplier(const float InPitchSemitones) { if (InPitchSemitones == 0.0f) { return 1.0f; } return FMath::Pow(2.0f, InPitchSemitones / 12.0f); } // Returns the number of semitones relative to a base frequency given the input frequency multiplier FORCEINLINE float GetSemitones(const float InMultiplier) { if (InMultiplier <= 0.0f) { return 12.0f * FMath::Log2(UE_SMALL_NUMBER); } return 12.0f * FMath::Log2(InMultiplier); } // Calculates equal power stereo pan using sinusoidal-panning law and cheap approximation for sin // InLinear pan is [-1.0, 1.0] so it can be modulated by a bipolar LFO FORCEINLINE void GetStereoPan(const float InLinearPan, float& OutLeft, float& OutRight) { const float LeftPhase = 0.5f * UE_PI * (0.5f * (InLinearPan + 1.0f) + 1.0f); const float RightPhase = 0.25f * UE_PI * (InLinearPan + 1.0f); OutLeft = FMath::Clamp(FastSin(LeftPhase), 0.0f, 1.0f); OutRight = FMath::Clamp(FastSin(RightPhase), 0.0f, 1.0f); } // Encodes a stereo Left/Right signal into a stereo Mid/Side signal FORCEINLINE void EncodeMidSide(float& LeftChannel, float& RightChannel) { const float Temp = (LeftChannel - RightChannel); //Output LeftChannel = (LeftChannel + RightChannel); RightChannel = Temp; } // Encodes a stereo Left/Right signal into a stereo Mid/Side signal SIGNALPROCESSING_API void EncodeMidSide( const FAlignedFloatBuffer& InLeftChannel, const FAlignedFloatBuffer& InRightChannel, FAlignedFloatBuffer& OutMidChannel, FAlignedFloatBuffer& OutSideChannel); // Decodes a stereo Mid/Side signal into a stereo Left/Right signal FORCEINLINE void DecodeMidSide(float& MidChannel, float& SideChannel) { const float Temp = (MidChannel - SideChannel) * 0.5f; //Output MidChannel = (MidChannel + SideChannel) * 0.5f; SideChannel = Temp; } // Decodes a stereo Mid/Side signal into a stereo Left/Right signal SIGNALPROCESSING_API void DecodeMidSide( const FAlignedFloatBuffer& InMidChannel, const FAlignedFloatBuffer& InSideChannel, FAlignedFloatBuffer& OutLeftChannel, FAlignedFloatBuffer& OutRightChannel); // Helper function to get bandwidth from Q FORCEINLINE float GetBandwidthFromQ(const float InQ) { // make sure Q is not 0.0f, clamp to slightly positive const float Q = FMath::Max(UE_KINDA_SMALL_NUMBER, InQ); const float Arg = 0.5f * ((1.0f / Q) + FMath::Sqrt(1.0f / (Q*Q) + 4.0f)); const float OutBandwidth = 2.0f * FMath::LogX(2.0f, Arg); return OutBandwidth; } // Helper function get Q from bandwidth FORCEINLINE float GetQFromBandwidth(const float InBandwidth) { const float InBandwidthClamped = FMath::Max(UE_KINDA_SMALL_NUMBER, InBandwidth); const float Temp = FMath::Pow(2.0f, InBandwidthClamped); const float OutQ = FMath::Sqrt(Temp) / (Temp - 1.0f); return OutQ; } // Given three values, determine peak location and value of quadratic fitted to the data. // // @param InValues - An array of 3 values with the maximum value located in InValues[1]. // @param OutPeakLoc - The peak location relative to InValues[1]. // @param OutPeakValue - The value of the peak at the peak location. // // @returns True if a peak was found, false if the values do not represent a peak. FORCEINLINE bool QuadraticPeakInterpolation(const float InValues[3], float& OutPeakLoc, float& OutPeakValue) { float Denom = InValues[0] - 2.f * InValues[1] + InValues[2]; if (Denom >= 0.f) { // This is not a peak. return false; } float Tmp = InValues[0] - InValues[2]; OutPeakLoc = 0.5f * Tmp / Denom; if ((OutPeakLoc > 0.5f) || (OutPeakLoc < -0.5f)) { // This is not a peak. return false; } OutPeakValue = InValues[1] - 0.25f * Tmp * OutPeakLoc; return true; } // Polynomial interpolation using lagrange polynomials. // https://en.wikipedia.org/wiki/Lagrange_polynomial FORCEINLINE float LagrangianInterpolation(const TArray Points, const float Alpha) { float Lagrangian = 1.0f; float Output = 0.0f; const int32 NumPoints = Points.Num(); for (int32 i = 0; i < NumPoints; ++i) { Lagrangian = 1.0f; for (int32 j = 0; j < NumPoints; ++j) { if (i != j) { float Denom = UE_REAL_TO_FLOAT(Points[i].X - Points[j].X); if (FMath::Abs(Denom) < UE_SMALL_NUMBER) { Denom = UE_SMALL_NUMBER; } Lagrangian *= (Alpha - UE_REAL_TO_FLOAT(Points[j].X)) / Denom; } } Output += Lagrangian * UE_REAL_TO_FLOAT(Points[i].Y); } return Output; } // Simple exponential easing class. Useful for cheaply and smoothly interpolating parameters. class FExponentialEase { public: FExponentialEase(float InInitValue = 0.0f, float InEaseFactor = 0.001f, float InThreshold = UE_KINDA_SMALL_NUMBER) : CurrentValue(InInitValue) , Threshold(InThreshold) , TargetValue(InInitValue) , EaseFactor(InEaseFactor) , OneMinusEase(1.0f - InEaseFactor) , EaseTimesTarget(EaseFactor * InInitValue) { } void Init(float InInitValue, float InEaseFactor = 0.001f) { CurrentValue = InInitValue; TargetValue = InInitValue; EaseFactor = InEaseFactor; OneMinusEase = 1.0f - EaseFactor; EaseTimesTarget = TargetValue * EaseFactor; } bool IsDone() const { return FMath::Abs(TargetValue - CurrentValue) < Threshold; } float GetNextValue() { if (IsDone()) { return CurrentValue; } // Micro-optimization, // But since GetNextValue(NumTicksToJumpAhead) does this work in a tight loop (non-vectorizable), might as well /* return CurrentValue = CurrentValue + (TargetValue - CurrentValue) * EaseFactor; = CurrentValue + EaseFactor*TargetValue - EaseFactor*CurrentValue = (CurrentValue - EaseFactor*CurrentValue) + EaseFactor*TargetValue = (1 - EaseFactor)*CurrentValue + EaseFactor*TargetValue */ return CurrentValue = OneMinusEase * CurrentValue + EaseTimesTarget; } // same as GetValue(), but overloaded to jump forward by NumTicksToJumpAhead timesteps // (before getting the value) float GetNextValue(uint32 NumTicksToJumpAhead) { while (NumTicksToJumpAhead && !IsDone()) { CurrentValue = OneMinusEase * CurrentValue + EaseTimesTarget; --NumTicksToJumpAhead; } return CurrentValue; } float PeekCurrentValue() const { return CurrentValue; } void SetEaseFactor(const float InEaseFactor) { EaseFactor = InEaseFactor; OneMinusEase = 1.0f - EaseFactor; } void operator=(const float& InValue) { SetValue(InValue); } void SetValue(const float InValue, const bool bIsInit = false) { TargetValue = InValue; EaseTimesTarget = EaseFactor * TargetValue; if (bIsInit) { CurrentValue = TargetValue; } } // This is a method for getting the factor to use for a given tau and sample rate. // Tau here is defined as the time it takes the interpolator to be within 1/e of it's destination. static float GetFactorForTau(float InTau, float InSampleRate) { return 1.0f - FMath::Exp(-1.0f / (InTau * InSampleRate)); } private: // Current value of the exponential ease float CurrentValue; // Threshold to use to evaluate if the ease is done float Threshold; // Target value float TargetValue; // Percentage to move toward target value from current value each tick float EaseFactor; // 1.0f - EaseFactor float OneMinusEase; // EaseFactor * TargetValue float EaseTimesTarget; }; // Simple easing function used to help interpolate params class FLinearEase { public: FLinearEase() : StartValue(0.0f) , CurrentValue(0.0f) , DeltaValue(0.0f) , SampleRate(44100.0f) , DurationTicks(0) , DefaultDurationTicks(0) , CurrentTick(0) , bIsInit(true) { } ~FLinearEase() { } bool IsDone() const { return CurrentTick >= DurationTicks; } void Init(float InSampleRate) { SampleRate = InSampleRate; bIsInit = true; } void SetValueRange(const float Start, const float End, const float InTimeSec) { StartValue = Start; CurrentValue = Start; SetValue(End, InTimeSec); } float GetNextValue() { if (IsDone()) { return CurrentValue; } CurrentValue = DeltaValue * (float)CurrentTick / (float)DurationTicks + StartValue; ++CurrentTick; return CurrentValue; } // same as GetValue(), but overloaded to increment Current Tick by NumTicksToJumpAhead // (before getting the value) float GetNextValue(int32 NumTicksToJumpAhead) { if (IsDone()) { return CurrentValue; } CurrentTick = FMath::Min(CurrentTick + NumTicksToJumpAhead, DurationTicks); CurrentValue = DeltaValue * (float)CurrentTick / (float)DurationTicks + StartValue; return CurrentValue; } float PeekCurrentValue() const { return CurrentValue; } // Updates the target value without changing the duration or tick data. // Sets the state as if the new value was the target value all along void SetValueInterrupt(const float InValue) { if (IsDone()) { CurrentValue = InValue; } else { DurationTicks = DurationTicks - CurrentTick; CurrentTick = 0; DeltaValue = InValue - CurrentValue; StartValue = CurrentValue; } } void SetValue(const float InValue, float InTimeSec = 0.0f) { if (bIsInit) { bIsInit = false; DurationTicks = 0; } else { DurationTicks = (int32)(SampleRate * InTimeSec); } CurrentTick = 0; if (DurationTicks == 0) { CurrentValue = InValue; } else { DeltaValue = InValue - CurrentValue; StartValue = CurrentValue; } } private: float StartValue; float CurrentValue; float DeltaValue; float SampleRate; int32 DurationTicks; int32 DefaultDurationTicks; int32 CurrentTick; bool bIsInit; }; // Simple parameter object which uses critical section to write to and read from data template class TParams { public: TParams() : bChanged(false) {} // Sets the params void SetParams(const T& InParams) { FScopeLock Lock(&CritSect); bChanged = true; CurrentParams = InParams; } // Returns a copy of the params safely if they've changed since last time this was called bool GetParams(T* OutParamsCopy) { FScopeLock Lock(&CritSect); if (bChanged) { bChanged = false; *OutParamsCopy = CurrentParams; return true; } return false; } void CopyParams(T& OutParamsCopy) const { FScopeLock Lock(&CritSect); OutParamsCopy = CurrentParams; } bool bChanged; T CurrentParams; mutable FCriticalSection CritSect; }; template struct DisjointedArrayView { DisjointedArrayView(TArrayView && InFirstBuffer, TArrayView && InSecondBuffer) : FirstBuffer(MoveTemp(InFirstBuffer)) , SecondBuffer(MoveTemp(InSecondBuffer)) {} template DisjointedArrayView SplitOtherToMatch(OtherSampleType* Other, int32 InNum) const { ensure(InNum == Num()); const int32 FirstChunkNum = FirstNum(); return DisjointedArrayView( TArrayView(Other, FirstChunkNum) , TArrayView(Other + FirstChunkNum, InNum - FirstChunkNum) ); } int32 CopyIntoBuffer(SampleType* InDestination, int32 InNumSamples) { check(InNumSamples >= Num()); const int32 FirstCopySize = FirstNum() * sizeof(SampleType); const int32 SecondCopySize = SecondNum() * sizeof(SampleType); FMemory::Memcpy(InDestination, FirstBuffer.GetData(), FirstCopySize); if (SecondCopySize) { FMemory::Memcpy(InDestination + FirstNum(), SecondBuffer.GetData(), SecondCopySize); } return Num(); } int32 FirstNum() const { return FirstBuffer.Num(); } int32 SecondNum() const { return SecondBuffer.Num(); } int32 Num() const { return FirstBuffer.Num() + SecondBuffer.Num(); } // data: TArrayView FirstBuffer; TArrayView SecondBuffer; }; // struct DisjointedArrayView /** * Basic implementation of a circular buffer built for pushing and popping arbitrary amounts of data at once. * Designed to be thread safe for SPSC; However, if Push() and Pop() are both trying to access an overlapping area of the buffer, * One of the calls will be truncated. Thus, it is advised that you use a high enough capacity that the producer and consumer are never in contention. */ template class TCircularAudioBuffer { private: TArray> InternalBuffer; uint32 Capacity; FThreadSafeCounter ReadCounter; FThreadSafeCounter WriteCounter; public: TCircularAudioBuffer() { SetCapacity(0); } TCircularAudioBuffer(const TCircularAudioBuffer& InOther) { *this = InOther; } TCircularAudioBuffer& operator=(const TCircularAudioBuffer& InOther) { InternalBuffer = InOther.InternalBuffer; Capacity = InOther.Capacity; ReadCounter.Set(InOther.ReadCounter.GetValue()); WriteCounter.Set(InOther.WriteCounter.GetValue()); return *this; } TCircularAudioBuffer(uint32 InCapacity) { SetCapacity(InCapacity); } void Reset(uint32 InCapacity = 0) { SetCapacity(InCapacity); } void Empty() { ReadCounter.Set(0); WriteCounter.Set(0); InternalBuffer.Empty(); } void SetCapacity(uint32 InCapacity) { checkf(InCapacity < (uint32)TNumericLimits::Max(), TEXT("Max capacity for this buffer is 2,147,483,647 samples. Otherwise our index arithmetic will not work.")); Capacity = InCapacity + 1; ReadCounter.Set(0); WriteCounter.Set(0); InternalBuffer.Reset(); InternalBuffer.AddZeroed(Capacity); } /** Reserve capacity. * * @param InMinimumCapacity - Minimum capacity of circular buffer. * @param bRetainExistingSamples - If true, existing samples in the buffer will be retained. If false, they are discarded. */ void Reserve(uint32 InMinimumCapacity, bool bRetainExistingSamples) { if (Capacity <= InMinimumCapacity) { uint32 NewCapacity = InMinimumCapacity + 1; checkf(NewCapacity < (uint32)TNumericLimits::Max(), TEXT("Max capacity overflow. Requested %d. Maximum allowed %d"), NewCapacity, TNumericLimits::Max()); uint32 NumToAdd = NewCapacity - Capacity; InternalBuffer.AddZeroed(NumToAdd); Capacity = NewCapacity; } if (!bRetainExistingSamples) { ReadCounter.Set(0); WriteCounter.Set(0); } } /** Push an array of values into circular buffer. */ int32 Push(TArrayView InBuffer) { return Push(InBuffer.GetData(), InBuffer.Num()); } // Pushes some amount of samples into this circular buffer. // Returns the amount of samples written. // This can only be used for trivially copyable types. int32 Push(const SampleType* InBuffer, uint32 NumSamples) { SampleType* DestBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); int32 NumToCopy = FMath::Min(NumSamples, Remainder()); const int32 NumToWrite = FMath::Min(NumToCopy, Capacity - WriteIndex); FMemory::Memcpy(&DestBuffer[WriteIndex], InBuffer, NumToWrite * sizeof(SampleType)); FMemory::Memcpy(&DestBuffer[0], &InBuffer[NumToWrite], (NumToCopy - NumToWrite) * sizeof(SampleType)); WriteCounter.Set((WriteIndex + NumToCopy) % Capacity); return NumToCopy; } // Pushes some amount of zeros into the circular buffer. // Useful when acting as a blocked, mono/interleaved delay line int32 PushZeros(uint32 NumSamplesOfZeros) { SampleType* DestBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); int32 NumToZeroEnd = FMath::Min(NumSamplesOfZeros, Remainder()); const int32 NumToZeroBegin = FMath::Min(NumToZeroEnd, Capacity - WriteIndex); FMemory::Memzero(&DestBuffer[WriteIndex], NumToZeroBegin * sizeof(SampleType)); FMemory::Memzero(&DestBuffer[0], (NumToZeroEnd - NumToZeroBegin) * sizeof(SampleType)); WriteCounter.Set((WriteIndex + NumToZeroEnd) % Capacity); return NumToZeroEnd; } // Push a single sample onto this buffer. // Returns false if the buffer is full. bool Push(const SampleType& InElement) { if (Remainder() == 0) { return false; } else { SampleType* DestBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); DestBuffer[WriteIndex] = InElement; WriteCounter.Set((WriteIndex + 1) % Capacity); return true; } } bool Push(SampleType && InElement) { if (Remainder() == 0) { return false; } else { SampleType* DestBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); DestBuffer[WriteIndex] = MoveTemp(InElement); WriteCounter.Set((WriteIndex + 1) % Capacity); return true; } } // Same as Pop(), but does not increment the read counter. int32 Peek(SampleType* OutBuffer, uint32 NumSamples) const { const SampleType* SrcBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); int32 NumToCopy = FMath::Min(NumSamples, Num()); const int32 NumRead = FMath::Min(NumToCopy, Capacity - ReadIndex); FMemory::Memcpy(OutBuffer, &SrcBuffer[ReadIndex], NumRead * sizeof(SampleType)); FMemory::Memcpy(&OutBuffer[NumRead], &SrcBuffer[0], (NumToCopy - NumRead) * sizeof(SampleType)); check(NumSamples < ((uint32)TNumericLimits::Max())); return NumToCopy; } // same Peek(), but provides a (possibly) disjointed view of the memory in-place // Push calls while the returned view is being accessed is undefined behavior DisjointedArrayView PeekInPlace(uint32 NumSamples) const { const SampleType* SrcBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); const uint32 WriteIndex = WriteCounter.GetValue(); int32 NumToView = FMath::Min(NumSamples, Num()); const int32 NumRead = FMath::Min(NumToView, Capacity - ReadIndex); check(NumSamples < ((uint32)TNumericLimits::Max())); return DisjointedArrayView < const SampleType > ( TArrayView(SrcBuffer + ReadIndex, NumRead) , TArrayView(SrcBuffer, (NumToView - NumRead)) ); } // Peeks a single element. // returns false if the element is empty. bool Peek(SampleType& OutElement) const { if (Num() == 0) { return false; } else { SampleType* SrcBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); OutElement = SrcBuffer[ReadIndex]; return true; } } // Pops some amount of samples into this circular buffer. // Returns the amount of samples read. int32 Pop(SampleType* OutBuffer, uint32 NumSamples) { int32 NumSamplesRead = Peek(OutBuffer, NumSamples); check(NumSamples < ((uint32)TNumericLimits::Max())); ReadCounter.Set((ReadCounter.GetValue() + NumSamplesRead) % Capacity); return NumSamplesRead; } // Same as Pop(), but provides a (possibly) disjinted view of memory in-place // Push calls while the returned view is being accessed is undefined behavior DisjointedArrayView PopInPlace(uint32 NumSamples) { check(NumSamples < ((uint32)TNumericLimits::Max())); DisjointedArrayView View = PeekInPlace(NumSamples); const int32 NumSamplesRead = View.Num(); ReadCounter.Set((ReadCounter.GetValue() + NumSamplesRead) % Capacity); return View; } // Pops some amount of samples into this circular buffer. // Returns the amount of samples read. int32 Pop(uint32 NumSamples) { check(NumSamples < ((uint32)TNumericLimits::Max())); int32 NumSamplesRead = FMath::Min(NumSamples, Num()); ReadCounter.Set((ReadCounter.GetValue() + NumSamplesRead) % Capacity); return NumSamplesRead; } // Pops a single element. // Will assert if the buffer is empty. Please check Num() > 0 before calling. SampleType Pop() { // Calling this when the buffer is empty is considered a fatal error. check(Num() > 0); SampleType* SrcBuffer = InternalBuffer.GetData(); const uint32 ReadIndex = ReadCounter.GetValue(); SampleType PoppedValue = MoveTempIfPossible(InternalBuffer[ReadIndex]); ReadCounter.Set((ReadCounter.GetValue() + 1) % Capacity); return PoppedValue; } // When called, seeks the read or write cursor to only retain either the NumSamples latest data // (if bRetainOldestSamples is false) or the NumSamples oldest data (if bRetainOldestSamples is true) // in the buffer. Cannot be used to increase the capacity of this buffer. void SetNum(uint32 NumSamples, bool bRetainOldestSamples = false) { check(NumSamples < Capacity); if (bRetainOldestSamples) { WriteCounter.Set((ReadCounter.GetValue() + NumSamples) % Capacity); } else { int64 ReadCounterNum = ((int32)WriteCounter.GetValue()) - ((int32) NumSamples); if (ReadCounterNum < 0) { ReadCounterNum = Capacity + ReadCounterNum; } ReadCounter.Set(ReadCounterNum); } } // Get number of samples that can be popped off of the buffer. uint32 Num() const { const int32 ReadIndex = ReadCounter.GetValue(); const int32 WriteIndex = WriteCounter.GetValue(); if (WriteIndex >= ReadIndex) { return WriteIndex - ReadIndex; } else { return Capacity - ReadIndex + WriteIndex; } } // Get the current capacity of the buffer uint32 GetCapacity() const { return Capacity; } // Get number of samples that can be pushed onto the buffer before it is full. uint32 Remainder() const { return Capacity - Num() - 1; } }; /** * This allows us to write a compile time exponent of a number. */ template struct TGetPower { static_assert(Exp >= 0, "TGetPower only supports positive exponents."); static const int64 Value = Base * TGetPower::Value; }; template struct TGetPower { static const int64 Value = 1; }; /** * TSample * Variant type to simplify converting and performing operations on fixed precision and floating point samples. */ template class TSample { SampleType Sample; template static void CheckValidityOfSampleType() { constexpr bool bIsTypeValid = !!(TIsFloatingPoint::Value || TIsIntegral::Value); static_assert(bIsTypeValid, "Invalid sample type! TSampleRef only supports float or integer values."); } template static void CheckValidityOfQ() { // If this is a fixed-precision value, our Q offset must be less than how many bits we have. constexpr bool bIsTypeValid = !!(TIsFloatingPoint::Value || ((sizeof(SampleTypeToCheck) * 8) > QToCheck)); static_assert(bIsTypeValid, "Invalid value for Q! TSampleRef only supports float or int types. For int types, Q must be smaller than the number of bits in the int type."); } // This is the number used to convert from float to our fixed precision value. static constexpr float QFactor = TGetPower<2, Q>::Value - 1; // for fixed precision types, the max and min values that we can represent are calculated here: static constexpr float MaxValue = TGetPower<2, (sizeof(SampleType) * 8 - Q)>::Value; static constexpr float MinValue = !!TIsSigned::Value ? (-1.0f * MaxValue) : 0.0f; public: TSample(SampleType& InSample) : Sample(InSample) { CheckValidityOfQ(); CheckValidityOfSampleType(); } template ReturnType AsFloat() const { static_assert(TIsFloatingPoint::Value, "Return type for AsFloat() must be a floating point type."); if (TIsFloatingPoint::Value) { return static_cast(Sample); } else if (TIsIntegral::Value) { // Cast from fixed to float. return static_cast(Sample) / QFactor; } else { checkNoEntry(); return static_cast(Sample); } } template ReturnType AsFixedPrecisionInt() { static_assert(TIsIntegral::Value, "This function must be called with an integer type as ReturnType."); CheckValidityOfQ(); if (TIsIntegral::Value) { if (Q > ReturnQ) { return Sample << (Q - ReturnQ); } else if (Q < ReturnQ) { return Sample >> (ReturnQ - Q); } else { return Sample; } } else if (TIsFloatingPoint::Value) { static constexpr float ReturnQFactor = TGetPower<2, ReturnQ>::Value - 1; return (ReturnType)(Sample * ReturnQFactor); } } template TSample& operator =(const OtherSampleType InSample) { CheckValidityOfSampleType(); if constexpr (std::is_same_v) { Sample = InSample; return *this; } else if (TIsIntegral::Value && TIsFloatingPoint::Value) { // Cast from Q15 to float. Sample = ((SampleType)InSample) / QFactor; return *this; } else if (TIsFloatingPoint::Value && TIsIntegral::Value) { // cast from float to Q15. Sample = static_cast(InSample * QFactor); return *this; } else { checkNoEntry(); return *this; } } template friend TSample operator *(const TSample& LHS, const OtherSampleType& RHS) { CheckValidityOfSampleType(); // Float case: if (TIsFloatingPoint::Value) { if (TIsFloatingPoint::Value) { // float * float. return LHS.Sample * RHS; } else if (TIsIntegral::Value) { // float * Q. SampleType FloatRHS = ((SampleType)RHS) / QFactor; return LHS.Sample * FloatRHS; } else { checkNoEntry(); return LHS.Sample; } } // Q Case else if (TIsIntegral::Value) { if (TIsFloatingPoint::Value) { // fixed * float. OtherSampleType FloatLHS = ((OtherSampleType)LHS.Sample) / QFactor; OtherSampleType Result = FMath::Clamp(FloatLHS * RHS, MinValue, MaxValue); return static_cast(Result * QFactor); } else if (TIsIntegral::Value) { // Q * Q. float FloatLHS = ((float)LHS.Sample) / QFactor; float FloatRHS = ((float)RHS) / QFactor; float Result = FMath::Clamp(FloatLHS * FloatRHS, MinValue, MaxValue); return static_cast(Result * QFactor); } else { checkNoEntry(); return LHS.Sample; } } else { checkNoEntry(); return LHS.Sample; } } }; /** * TSampleRef * Ref version of TSample. Useful for converting between fixed and float precisions. * Example usage: * int16 FixedPrecisionSample; * TSampleRef SampleRef(FixedPrecisionSample); * * // Set the sample value directly: * SampleRef = 0.5f; * * // Or multiply the the sample: * SampleRef *= 0.5f; * * bool bThisCodeWorks = FixedPrecisionSample == TNumericLimits::Max() / 4; */ template class TSampleRef { SampleType& Sample; template static void CheckValidityOfSampleType() { constexpr bool bIsTypeValid = !!(TIsFloatingPoint::Value || TIsIntegral::Value); static_assert(bIsTypeValid, "Invalid sample type! TSampleRef only supports float or integer values."); } template static void CheckValidityOfQ() { // If this is a fixed-precision value, our Q offset must be less than how many bits we have. constexpr bool bIsTypeValid = !!(TIsFloatingPoint::Value || (sizeof(SampleTypeToCheck) * 8) > QToCheck); static_assert(bIsTypeValid, "Invalid value for Q! TSampleRef only supports float or int types. For int types, Q must be smaller than the number of bits in the int type."); } // This is the number used to convert from float to our fixed precision value. static constexpr float QFactor = TGetPower<2, Q>::Value - 1; // for fixed precision types, the max and min values that we can represent are calculated here: static constexpr float MaxValue = TGetPower<2, (sizeof(SampleType) * 8 - Q)>::Value; static constexpr float MinValue = !!TIsSigned::Value ? (-1.0f * MaxValue) : 0.0f; public: TSampleRef(SampleType& InSample) : Sample(InSample) { CheckValidityOfQ(); CheckValidityOfSampleType(); } template ReturnType AsFloat() const { static_assert(TIsFloatingPoint::Value, "Return type for AsFloat() must be a floating point type."); if (TIsFloatingPoint::Value) { return static_cast(Sample); } else if (TIsIntegral::Value) { // Cast from fixed to float. return static_cast(Sample) / QFactor; } else { checkNoEntry(); return static_cast(Sample); } } template ReturnType AsFixedPrecisionInt() { static_assert(TIsIntegral::Value, "This function must be called with an integer type as ReturnType."); CheckValidityOfQ(); if (TIsIntegral::Value) { if (Q > ReturnQ) { return Sample << (Q - ReturnQ); } else if (Q < ReturnQ) { return Sample >> (ReturnQ - Q); } else { return Sample; } } else if (TIsFloatingPoint::Value) { static constexpr SampleType ReturnQFactor = TGetPower<2, ReturnQ>::Value - 1; return (ReturnType) (Sample * ReturnQFactor); } } template TSampleRef& operator =(const OtherSampleType InSample) { CheckValidityOfSampleType(); if constexpr (std::is_same_v) { Sample = InSample; return *this; } else if (TIsIntegral::Value && TIsFloatingPoint::Value) { // Cast from fixed to float. Sample = ((SampleType)InSample) / QFactor; return *this; } else if (TIsFloatingPoint::Value && TIsIntegral::Value) { // cast from float to fixed. Sample = (SampleType)(InSample * QFactor); return *this; } else { checkNoEntry(); return *this; } } template friend SampleType operator *(const TSampleRef& LHS, const OtherSampleType& RHS) { CheckValidityOfSampleType(); // Float case: if (TIsFloatingPoint::Value) { if (TIsFloatingPoint::Value) { // float * float. return LHS.Sample * RHS; } else if (TIsIntegral::Value) { // float * fixed. SampleType FloatRHS = ((SampleType)RHS) / QFactor; return LHS.Sample * FloatRHS; } else { checkNoEntry(); return LHS.Sample; } } // Fixed Precision Case else if (TIsIntegral::Value) { if (TIsFloatingPoint::Value) { // fixed * float. OtherSampleType FloatLHS = ((OtherSampleType)LHS.Sample) / QFactor; OtherSampleType Result = FMath::Clamp(FloatLHS * RHS, MinValue, MaxValue); return static_cast(Result * QFactor); } else if (TIsIntegral::Value) { // fixed * fixed. float FloatLHS = ((float)LHS.Sample) / QFactor; float FloatRHS = ((float)RHS) / QFactor; float Result = FMath::Clamp(FloatLHS * FloatRHS, MinValue, MaxValue); return static_cast(Result * QFactor); } else { checkNoEntry(); return LHS.Sample; } } else { checkNoEntry(); return LHS.Sample; } } }; }