// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/DynamicsProcessor.h" #include "DSP/FloatArrayMath.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "SignalProcessingModule.h" namespace Audio { FDynamicsProcessor::FDynamicsProcessor() : ProcessingMode(EDynamicsProcessingMode::Compressor) , SlopeFactor(0.0f) , EnvelopeFollowerPeakMode(EPeakMode::Peak) , LookaheadDelayMsec(10.0f) , AttackTimeMsec(20.0f) , ReleaseTimeMsec(1000.0f) , ThresholdDb(-6.0f) , Ratio(1.0f) , HalfKneeBandwidthDb(5.0f) , InputGain(1.0f) , OutputGain(1.0f) , KeyGain(1.0f) , SampleRate(48000.f) , LinkMode(EDynamicsProcessorChannelLinkMode::Disabled) , bIsAnalogMode(true) , bKeyAuditionEnabled(false) , bKeyHighshelfEnabled(false) , bKeyLowshelfEnabled(false) { // The knee will have 2 points KneePoints.Init(FKneePoint(), 2); CalculateSlope(); } FDynamicsProcessor::~FDynamicsProcessor() { } void FDynamicsProcessor::Init(const float InSampleRate, const int32 InNumChannels) { check(InSampleRate > 0.f); SampleRate = InSampleRate; SetNumChannels(InNumChannels); SetKeyNumChannels(InNumChannels); for (int32 Channel = 0; Channel < InNumChannels; ++Channel) { EnvFollower[Channel].Init(FInlineEnvelopeFollowerInitParams{SampleRate, AttackTimeMsec, ReleaseTimeMsec, EnvelopeFollowerPeakMode, bIsAnalogMode}); } // Initialize key filters InputLowshelfFilter.Init(SampleRate, InNumChannels, EBiquadFilter::LowShelf); InputHighshelfFilter.Init(SampleRate, InNumChannels, EBiquadFilter::HighShelf); DetectorOuts.Reset(); DetectorOuts.AddZeroed(InNumChannels); Gain.Reset(); Gain.AddZeroed(InNumChannels); } int32 FDynamicsProcessor::GetNumChannels() const { return Gain.Num(); } int32 FDynamicsProcessor::GetKeyNumChannels() const { return EnvFollower.Num(); } float Audio::FDynamicsProcessor::GetMaxLookaheadMsec() const { return MaxLookaheadMsec; } void FDynamicsProcessor::SetLookaheadMsec(const float InLookAheadMsec) { LookaheadDelayMsec = FMath::Min(InLookAheadMsec, MaxLookaheadMsec); const int32 NumDelayFrames = GetNumDelayFrames(); for (int32 Channel = 0; Channel < LookaheadDelay.Num(); ++Channel) { LookaheadDelay[Channel].SetDelayLengthSamples(NumDelayFrames); } } void FDynamicsProcessor::SetAttackTime(const float InAttackTimeMsec) { AttackTimeMsec = InAttackTimeMsec; for (int32 Channel = 0; Channel < EnvFollower.Num(); ++Channel) { EnvFollower[Channel].SetAttackTime(InAttackTimeMsec); } } void FDynamicsProcessor::SetReleaseTime(const float InReleaseTimeMsec) { ReleaseTimeMsec = InReleaseTimeMsec; for (int32 Channel = 0; Channel < EnvFollower.Num(); ++Channel) { EnvFollower[Channel].SetReleaseTime(InReleaseTimeMsec); } } void FDynamicsProcessor::SetThreshold(const float InThresholdDb) { ThresholdDb = InThresholdDb; CalculateKnee(); } void FDynamicsProcessor::SetRatio(const float InCompressionRatio) { // Don't let the compression ratio be 0.0! Ratio = FMath::Max(InCompressionRatio, SMALL_NUMBER); CalculateSlope(); } void FDynamicsProcessor::SetKneeBandwidth(const float InKneeBandwidthDb) { HalfKneeBandwidthDb = 0.5f * InKneeBandwidthDb; CalculateKnee(); } void FDynamicsProcessor::SetInputGain(const float InInputGainDb) { InputGain = ConvertToLinear(InInputGainDb); } void FDynamicsProcessor::SetKeyAudition(const bool InAuditionEnabled) { bKeyAuditionEnabled = InAuditionEnabled; } void FDynamicsProcessor::SetKeyGain(const float InKeyGain) { KeyGain = ConvertToLinear(InKeyGain); } void FDynamicsProcessor::SetKeyHighshelfCutoffFrequency(const float InCutoffFreq) { InputHighshelfFilter.SetFrequency(InCutoffFreq); } void FDynamicsProcessor::SetKeyHighshelfEnabled(const bool bInEnabled) { bKeyHighshelfEnabled = bInEnabled; } void FDynamicsProcessor::SetKeyHighshelfGain(const float InGainDb) { InputHighshelfFilter.SetGainDB(InGainDb); } void FDynamicsProcessor::SetKeyLowshelfCutoffFrequency(const float InCutoffFreq) { InputLowshelfFilter.SetFrequency(InCutoffFreq); } void FDynamicsProcessor::SetKeyLowshelfEnabled(const bool bInEnabled) { bKeyLowshelfEnabled = bInEnabled; } void FDynamicsProcessor::SetKeyLowshelfGain(const float InGainDb) { InputLowshelfFilter.SetGainDB(InGainDb); } void FDynamicsProcessor::SetKeyNumChannels(const int32 InNumChannels) { if (InNumChannels != EnvFollower.Num()) { EnvFollower.Reset(); for (int32 Channel = 0; Channel < InNumChannels; ++Channel) { EnvFollower.Emplace(FInlineEnvelopeFollowerInitParams{SampleRate, AttackTimeMsec, ReleaseTimeMsec, EnvelopeFollowerPeakMode, bIsAnalogMode}); } } if (InNumChannels != InputLowshelfFilter.GetNumChannels()) { InputLowshelfFilter.Init(SampleRate, InNumChannels, EBiquadFilter::LowShelf); } if (InNumChannels != InputHighshelfFilter.GetNumChannels()) { InputHighshelfFilter.Init(SampleRate, InNumChannels, EBiquadFilter::HighShelf); } if (InNumChannels != DetectorOuts.Num()) { DetectorOuts.Reset(); DetectorOuts.AddZeroed(InNumChannels); } } void FDynamicsProcessor::SetOutputGain(const float InOutputGainDb) { OutputGain = ConvertToLinear(InOutputGainDb); } void FDynamicsProcessor::SetChannelLinkMode(const EDynamicsProcessorChannelLinkMode InLinkMode) { LinkMode = InLinkMode; } void FDynamicsProcessor::SetAnalogMode(const bool bInIsAnalogMode) { bIsAnalogMode = bInIsAnalogMode; for (int32 Channel = 0; Channel < EnvFollower.Num(); ++Channel) { EnvFollower[Channel].SetAnalog(bInIsAnalogMode); } } void FDynamicsProcessor::SetNumChannels(const int32 InNumChannels) { if (InNumChannels != Gain.Num()) { Gain.Reset(); Gain.AddZeroed(InNumChannels); } if (InNumChannels != LookaheadDelay.Num()) { // Initialize lookahead buffers constexpr int32 DelayInternalBufferSize = 32; const int32 MaxNumDelayFrames = FMath::Max(1, FMath::CeilToInt(MaxLookaheadMsec * SampleRate / 1000.0f)); const int32 NumDelayFrames = GetNumDelayFrames(); LookaheadDelay.Reset(); for (int32 Channel = 0; Channel < InNumChannels; ++Channel) { LookaheadDelay.Emplace(MaxNumDelayFrames, NumDelayFrames, DelayInternalBufferSize); } } } void FDynamicsProcessor::SetPeakMode(const EPeakMode::Type InEnvelopeFollowerModeType) { EnvelopeFollowerPeakMode = InEnvelopeFollowerModeType; for (int32 Channel = 0; Channel < EnvFollower.Num(); ++Channel) { EnvFollower[Channel].SetMode(EnvelopeFollowerPeakMode); } } void FDynamicsProcessor::SetProcessingMode(const EDynamicsProcessingMode::Type InProcessingMode) { ProcessingMode = InProcessingMode; CalculateSlope(); } void FDynamicsProcessor::ProcessAudioFrame(const float* InFrame, float* OutFrame, const float* InKeyFrame) { const bool bKeyIsInput = InFrame == InKeyFrame; if (ProcessKeyFrame(InKeyFrame, OutFrame, bKeyIsInput)) { const int32 NumChannels = GetNumChannels(); for (int32 Channel = 0; Channel < NumChannels; ++Channel) { // Write and read into the look ahead delay line. // We apply the compression output of the direct input to the output of this delay line // This way sharp transients can be "caught" with the gain. float LookaheadOutput = LookaheadDelay[Channel].ProcessAudioSample(InFrame[Channel]); // Write into the output with the computed gain value OutFrame[Channel] = Gain[Channel] * LookaheadOutput * OutputGain * InputGain; } } } void FDynamicsProcessor::ProcessAudioFrame(const float* InFrame, float* OutFrame, const float* InKeyFrame, float* OutGain) { check(OutGain != nullptr); const bool bKeyIsInput = InFrame == InKeyFrame; if (ProcessKeyFrame(InKeyFrame, OutFrame, bKeyIsInput)) { const int32 NumChannels = GetNumChannels(); for (int32 Channel = 0; Channel < NumChannels; ++Channel) { // Write and read into the look ahead delay line. // We apply the compression output of the direct input to the output of this delay line // This way sharp transients can be "caught" with the gain. float LookaheadOutput = LookaheadDelay[Channel].ProcessAudioSample(InFrame[Channel]); // Write into the output with the computed gain value OutFrame[Channel] = Gain[Channel] * LookaheadOutput * OutputGain * InputGain; // Also write the output gain value OutGain[Channel] = Gain[Channel]; } } } void FDynamicsProcessor::ProcessAudio(const float* InBuffer, const int32 InNumSamples, float* OutBuffer, const float* InKeyBuffer, float* OutEnvelope) { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudio_Interleaved); constexpr int32 MaxNumChannels = 8; constexpr int32 MinNumSubbufferFrames = 32; constexpr int32 NumSubbufferSamples = MaxNumChannels * MinNumSubbufferFrames; static_assert((NumSubbufferSamples * sizeof(float) * 4) <= 4096, "Statically allocated deinterleave buffers are clamped to be 4096 to protect against running out of stack memory."); // Stack allocated intermediate buffers to hold deinterleave audio float* DeinterleaveInput[MaxNumChannels]; float InputWorkBuffer[NumSubbufferSamples]; float* DeinterleaveOutput[MaxNumChannels]; float OutputWorkBuffer[NumSubbufferSamples]; float* DeinterleaveKey[MaxNumChannels]; float KeyWorkBuffer[NumSubbufferSamples]; float* DeinterleaveEnvelope[MaxNumChannels]; float EnvelopeWorkBuffer[NumSubbufferSamples]; const int32 NumChannels = GetNumChannels(); const int32 KeyNumChannels = GetKeyNumChannels(); const int32 NumInputFrames = InNumSamples / NumChannels; const int32 NumSubbufferFrames = (NumSubbufferSamples / FMath::Max(1, FMath::Max(NumChannels, KeyNumChannels))); check(nullptr != InBuffer); check(nullptr != OutBuffer); check(KeyNumChannels <= MaxNumChannels); check(NumChannels <= MaxNumChannels); check(NumSubbufferFrames >= MinNumSubbufferFrames); // Initialize deinterleave buffer pointers for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++) { DeinterleaveInput[ChannelIndex] = &InputWorkBuffer[ChannelIndex * NumSubbufferFrames]; DeinterleaveOutput[ChannelIndex] = &OutputWorkBuffer[ChannelIndex * NumSubbufferFrames]; DeinterleaveEnvelope[ChannelIndex] = &EnvelopeWorkBuffer[ChannelIndex * NumSubbufferFrames]; }; for (int32 KeyChannelIndex = 0; KeyChannelIndex < KeyNumChannels; KeyChannelIndex++) { DeinterleaveKey[KeyChannelIndex] = &KeyWorkBuffer[KeyChannelIndex * NumSubbufferFrames]; } // Run dyanmics processor on deinterleaved subbuffers. for (int32 FrameIndex = 0; FrameIndex < NumInputFrames; FrameIndex += NumSubbufferFrames) { const int32 NumFramesThisIteration = FMath::Min(NumSubbufferFrames, NumInputFrames - FrameIndex); const int32 InterleavedSampleIndex = FrameIndex * NumChannels; ArrayDeinterleave(&InBuffer[InterleavedSampleIndex], DeinterleaveInput, NumFramesThisIteration, NumChannels); if (InKeyBuffer) { const int32 KeyInterleavedSampleIndex = FrameIndex * KeyNumChannels; ArrayDeinterleave(&InKeyBuffer[KeyInterleavedSampleIndex], DeinterleaveKey, NumFramesThisIteration, KeyNumChannels); ProcessAudio(DeinterleaveInput, NumFramesThisIteration, DeinterleaveOutput, DeinterleaveKey, DeinterleaveEnvelope); } else { ProcessAudio(DeinterleaveInput, NumFramesThisIteration, DeinterleaveOutput, nullptr /* InKeyBuffers */, DeinterleaveEnvelope); } // Interleave output results if (OutEnvelope) { ArrayInterleave(DeinterleaveEnvelope, &OutEnvelope[InterleavedSampleIndex], NumFramesThisIteration, NumChannels); } ArrayInterleave(DeinterleaveOutput, &OutBuffer[InterleavedSampleIndex], NumFramesThisIteration, NumChannels); } } void FDynamicsProcessor::ProcessAudio(const float* const* const InBuffers, const int32 InNumFrames, float* const* OutBuffers, const float* const* const InKeyBuffers, float* const* OutEnvelopes) { // we need this test because we are going to use the output buffer as our scratch pad for key processing! check(GetKeyNumChannels() <= GetNumChannels()); ProcessAudioDeinterleaveInternal(InBuffers, InNumFrames, OutBuffers, InKeyBuffers, OutEnvelopes); } void FDynamicsProcessor::ProcessAudioDeinterleaveInternal(const float* const* const InBuffers, const int32 InNumFrames, float* const* OutBuffers, const float* const* const InKeyBuffers, float* const* OutEnvelopes) { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudioDeinterleaveInternal); // NOTE: This is a useful test, but it isn't comprehensive. It can't be sure the passed // in arrays of pointers have a matching number of pointers to the channel counts. It // will have to do for now. check(nullptr != InBuffers && nullptr != InBuffers[0]); check(nullptr != OutBuffers && nullptr != OutBuffers[0]); check(nullptr != OutEnvelopes && nullptr != OutEnvelopes[0]); // NOTE: The OutBuffers are used as scratch buffers for any processing applied // to the key. Callers must provide const int32 KeyNumChannels = GetKeyNumChannels(); const int32 NumChannels = GetNumChannels(); const float* const* KeySources = InKeyBuffers ? InKeyBuffers : InBuffers; float* const* KeyWorkBuffers = OutBuffers; // Process Key if (bKeyLowshelfEnabled) { InputLowshelfFilter.ProcessAudio(KeySources, InNumFrames, KeyWorkBuffers); KeySources = KeyWorkBuffers; } if (bKeyHighshelfEnabled) { InputHighshelfFilter.ProcessAudio(KeySources, InNumFrames, KeyWorkBuffers); KeySources = KeyWorkBuffers; } const bool bKeyIsInput = !InKeyBuffers || InKeyBuffers == InBuffers; float DetectorGain = InputGain; // Apply key gain only if detector is key (not input) if (!bKeyIsInput) { DetectorGain *= KeyGain; } if (bKeyAuditionEnabled) { if (KeySources != KeyWorkBuffers) { // This means we have not done either filter above, so we have not // "moved" the key input to the output buffer. We have to do it here. for (int32 Channel = 0; Channel < KeyNumChannels; ++Channel) { FMemory::Memcpy(OutBuffers[Channel], KeySources[Channel], sizeof(float) * InNumFrames); } } // we just have to zero out any channels that are not in the key signal for (int32 Channel = KeyNumChannels; Channel < NumChannels; ++Channel) { FMemory::Memset(OutBuffers[Channel], 0, sizeof(float) * InNumFrames); } // I WOULD zero or 1 the "OutEnvelope", but that is not what the original does! return; } for (int32 Channel = 0; Channel < KeyNumChannels; ++Channel) { // apply detector gain to key Audio::ArrayMultiplyByConstant(TArrayView(KeySources[Channel], InNumFrames), DetectorGain, TArrayView(KeyWorkBuffers[Channel], InNumFrames)); // EnvelopeFollow key EnvFollower[Channel].ProcessBuffer(KeyWorkBuffers[Channel], InNumFrames, OutEnvelopes[Channel]); } int32 NumGainChannels = 1; switch (LinkMode) { case EDynamicsProcessorChannelLinkMode::Average: { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudioDeinterleaveInternal_LinkModeAverage); // average all key channels TArrayView Out = TArrayView(OutEnvelopes[0], InNumFrames); for (int32 KeyChannel = 1; KeyChannel < KeyNumChannels; ++KeyChannel) { ArraySum(TArrayView(OutEnvelopes[KeyChannel], InNumFrames), Out, Out); } ArrayMultiplyByConstant(Out, 1.0f / (float)KeyNumChannels, Out); // convert magnitude to db ArrayMagnitudeToDecibelInPlace(Out, -96.0f); // compute 1 gain and use for all channels ... const float ComputedGain = ComputeGain(DetectorOutLinkedDb); ComputeGains(OutEnvelopes[0], InNumFrames); NumGainChannels = 1; } break; case EDynamicsProcessorChannelLinkMode::Peak: { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudioDeinterleaveInternal_LinkModePeak); // detect peak across all key channels TArrayView Out = TArrayView(OutEnvelopes[0], InNumFrames); for (int32 KeyChannel = 1; KeyChannel < KeyNumChannels; ++KeyChannel) { ArrayMax(TArrayView(OutEnvelopes[KeyChannel], InNumFrames), Out, Out); } // convert magnitude to db ArrayMagnitudeToDecibelInPlace(Out, -96.0f); // compute 1 gain and use for all channels ... const float ComputedGain = ComputeGain(DetectorOutLinkedDb); ComputeGains(OutEnvelopes[0], InNumFrames); NumGainChannels = 1; } break; case EDynamicsProcessorChannelLinkMode::Disabled: default: { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudioDeinterleaveInternal_LinkModeDisabled); for (int32 Channel = 0; Channel < KeyNumChannels; ++Channel) { // convert magnitude to db ArrayMagnitudeToDecibelInPlace(TArrayView(OutEnvelopes[Channel],InNumFrames), -96.0f); // compute 1 gain and use for all channels ... const float ComputedGain = ComputeGain(DetectorOutLinkedDb); ComputeGains(OutEnvelopes[Channel], InNumFrames); } NumGainChannels = KeyNumChannels; } break; } { TRACE_CPUPROFILER_EVENT_SCOPE(FDynamicsProcessor::ProcessAudioDeinterleaveInternal_ApplyGain); const float OutAndInGain = OutputGain * InputGain; for (int32 Channel = 0; Channel < NumChannels; ++Channel) { int32 GainChannel = Channel % NumGainChannels; TArrayView InBufferView = MakeArrayView(InBuffers[Channel], InNumFrames); TArrayView OutBufferView = MakeArrayView(OutBuffers[Channel], InNumFrames); TArrayView EnvelopeBufferView = MakeArrayView(OutEnvelopes[GainChannel], InNumFrames); // copy input to look ahead delay write position LookaheadDelay[Channel].ProcessAudio(InBufferView, OutBufferView); // apply compression gain & output gain & input gain to look ahead delay read position and save to output ArrayMultiplyInPlace(EnvelopeBufferView, OutBufferView); ArrayMultiplyByConstantInPlace(OutBufferView, OutAndInGain); } } } bool FDynamicsProcessor::ProcessKeyFrame(const float* InKeyFrame, float* OutFrame, bool bKeyIsInput) { // Get detector outputs const float* KeyIn = InKeyFrame; const int32 KeyNumChannels = GetKeyNumChannels(); const int32 NumChannels = GetNumChannels(); if (KeyNumChannels > 0) { if (bKeyLowshelfEnabled) { InputLowshelfFilter.ProcessAudioFrame(KeyIn, DetectorOuts.GetData()); KeyIn = DetectorOuts.GetData(); } if (bKeyHighshelfEnabled) { InputHighshelfFilter.ProcessAudioFrame(KeyIn, DetectorOuts.GetData()); KeyIn = DetectorOuts.GetData(); } } float DetectorGain = InputGain; // Apply key gain only if detector is key (not input) if (!bKeyIsInput) { DetectorGain *= KeyGain; } if (bKeyAuditionEnabled) { for (int32 Channel = 0; Channel < NumChannels; ++Channel) { const int32 KeyIndex = Channel % KeyNumChannels; OutFrame[Channel] = DetectorGain * KeyIn[KeyIndex]; } return false; } for (int32 Channel = 0; Channel < KeyNumChannels; ++Channel) { DetectorOuts[Channel] = EnvFollower[Channel].ProcessSample(DetectorGain * KeyIn[Channel]); } switch (LinkMode) { case EDynamicsProcessorChannelLinkMode::Average: { float KeyOutLinked = 0.0f; for (int32 Channel = 0; Channel < KeyNumChannels; ++Channel) { KeyOutLinked += DetectorOuts[Channel]; } KeyOutLinked /= static_cast(KeyNumChannels); const float DetectorOutLinkedDb = ConvertToDecibels(KeyOutLinked); const float ComputedGain = ComputeGain(DetectorOutLinkedDb); for (int32 Channel = 0; Channel < NumChannels; ++Channel) { Gain[Channel] = ComputedGain; } } break; case EDynamicsProcessorChannelLinkMode::Peak: { float KeyOutLinked = FMath::Max(DetectorOuts); const float KeyOutLinkedDb = ConvertToDecibels(KeyOutLinked); const float ComputedGain = ComputeGain(KeyOutLinkedDb); for (int32 Channel = 0; Channel < NumChannels; ++Channel) { Gain[Channel] = ComputedGain; } } break; case EDynamicsProcessorChannelLinkMode::Disabled: default: { // Compute gain individually per channel and wrap if // channel count is greater than key channel count. for (int32 Channel = 0; Channel < NumChannels; ++Channel) { const int32 KeyIndex = Channel % KeyNumChannels; float ChannelGain = DetectorOuts[KeyIndex]; const float KeyOutDb = ConvertToDecibels(ChannelGain); const float ComputedGain = ComputeGain(KeyOutDb); Gain[KeyIndex] = ComputedGain; } } break; } return true; } bool FDynamicsProcessor::IsInProcessingThreshold(const float InEnvFollowerDb) const { if (ProcessingMode == EDynamicsProcessingMode::UpwardsCompressor) { return HalfKneeBandwidthDb >= 0.0f && InEnvFollowerDb < (ThresholdDb - HalfKneeBandwidthDb) && InEnvFollowerDb > (ThresholdDb + HalfKneeBandwidthDb); } return HalfKneeBandwidthDb >= 0.0f && InEnvFollowerDb > (ThresholdDb - HalfKneeBandwidthDb) && InEnvFollowerDb < (ThresholdDb + HalfKneeBandwidthDb); } float FDynamicsProcessor::ComputeGain(const float InEnvFollowerDb) { // If we are in the range of compression float SlopeThisSample = SlopeFactor; if (IsInProcessingThreshold(InEnvFollowerDb)) { // The knee calculation adjusts the slope to use via lagrangian interpolation through the slope float Lagrangian = (InEnvFollowerDb - KneePoints[1].X) / Denominator0Minus1; SlopeThisSample = Lagrangian * KneePoints[0].Y; Lagrangian = (InEnvFollowerDb - KneePoints[0].X) / Denominator1Minus0; SlopeThisSample += Lagrangian * KneePoints[1].Y; } float OutputGainDb = SlopeThisSample * (ThresholdDb - InEnvFollowerDb); if (ProcessingMode == EDynamicsProcessingMode::UpwardsCompressor) { // if left unchecked Upwards compression will try to apply infinite gain OutputGainDb = FMath::Clamp(OutputGainDb, 0.f, UpwardsCompressionMaxGain); } else { OutputGainDb = FMath::Min(0.f, OutputGainDb); } return ConvertToLinear(OutputGainDb); } void FDynamicsProcessor::ComputeGains(float* InEnvFollowerDbOutGain, const int32 InNumSamples) { for (int32 SampleIndex = 0; SampleIndex < InNumSamples; ++SampleIndex) { float InEnvFollowerDb = InEnvFollowerDbOutGain[SampleIndex]; float SlopeThisSample = SlopeFactor; if (IsInProcessingThreshold(InEnvFollowerDb)) { float Lagrangian = (InEnvFollowerDb - KneePoints[1].X) / Denominator0Minus1; SlopeThisSample = Lagrangian * KneePoints[0].Y; Lagrangian = (InEnvFollowerDb - KneePoints[0].X) / Denominator1Minus0; SlopeThisSample += Lagrangian * KneePoints[1].Y; }; float OutputGainDb = SlopeThisSample * (ThresholdDb - InEnvFollowerDb); if (ProcessingMode == EDynamicsProcessingMode::UpwardsCompressor) { // if left unchecked Upwards compression will try to apply infinite gain OutputGainDb = FMath::Clamp(OutputGainDb, 0.f, UpwardsCompressionMaxGain); InEnvFollowerDbOutGain[SampleIndex] = ConvertToLinear(OutputGainDb); } else { InEnvFollowerDbOutGain[SampleIndex] = OutputGainDb > -UE_SMALL_NUMBER ? 1.0f : ConvertToLinear(OutputGainDb); } } } int32 FDynamicsProcessor::GetNumDelayFrames() const { checkf(LookaheadDelayMsec <= MaxLookaheadMsec, TEXT("An lookahead delay of %fms exceeds maximum lookahead delay of %fms"), LookaheadDelayMsec, MaxLookaheadMsec) return FMath::Max(0, FMath::CeilToInt(LookaheadDelayMsec * SampleRate / 1000.0f)); } void FDynamicsProcessor::CalculateSlope() { SlopeFactor = 0.0f; // Depending on the mode, we define the "slope". switch (ProcessingMode) { default: // Compressors smoothly reduce the gain as the gain gets louder // CompressionRatio -> Inifinity is a limiter // Upwards compression applies gain when below a threshold, but uses the same slope case EDynamicsProcessingMode::UpwardsCompressor: case EDynamicsProcessingMode::Compressor: SlopeFactor = 1.0f - 1.0f / Ratio; break; // Limiters do nothing until it hits the threshold then clamps the output hard case EDynamicsProcessingMode::Limiter: SlopeFactor = 1.0f; break; // Expanders smoothly increase the gain as the gain gets louder // CompressionRatio -> Infinity is a gate case EDynamicsProcessingMode::Expander: SlopeFactor = 1.0f / Ratio - 1.0f; break; // Gates are opposite of limiter. They stop sound (stop gain) until the threshold is hit case EDynamicsProcessingMode::Gate: SlopeFactor = -1.0f; break; } CalculateKnee(); } void FDynamicsProcessor::CalculateKnee() { // Setup the knee for interpolation. Don't allow the top knee point to exceed 0.0 KneePoints[0].X = ThresholdDb - HalfKneeBandwidthDb; KneePoints[1].X = FMath::Min(ThresholdDb + HalfKneeBandwidthDb, 0.0f); KneePoints[0].Y = 0.0f; KneePoints[1].Y = SlopeFactor; // These next few calculations let us optimize out the call to // the LagrangeInterpolation that used to be in there. We precalculate // some coefficients that remain the same unless the KneePoints change. Denominator0Minus1 = KneePoints[0].X - KneePoints[1].X; if (FMath::Abs(Denominator0Minus1) < UE_SMALL_NUMBER) { Denominator0Minus1 = UE_SMALL_NUMBER; } Denominator1Minus0 = KneePoints[1].X - KneePoints[0].X; if (FMath::Abs(Denominator1Minus0) < UE_SMALL_NUMBER) { Denominator1Minus0 = UE_SMALL_NUMBER; } } }