Files
UnrealEngine/Engine/Source/Runtime/SignalProcessing/Private/DynamicsProcessor.cpp
2025-05-18 13:04:45 +08:00

765 lines
25 KiB
C++

// 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<const float>(KeySources[Channel], InNumFrames), DetectorGain, TArrayView<float>(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<float> Out = TArrayView<float>(OutEnvelopes[0], InNumFrames);
for (int32 KeyChannel = 1; KeyChannel < KeyNumChannels; ++KeyChannel)
{
ArraySum(TArrayView<const float>(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<float> Out = TArrayView<float>(OutEnvelopes[0], InNumFrames);
for (int32 KeyChannel = 1; KeyChannel < KeyNumChannels; ++KeyChannel)
{
ArrayMax(TArrayView<const float>(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<float>(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<const float> InBufferView = MakeArrayView<const float>(InBuffers[Channel], InNumFrames);
TArrayView<float> OutBufferView = MakeArrayView<float>(OutBuffers[Channel], InNumFrames);
TArrayView<const float> EnvelopeBufferView = MakeArrayView<const float>(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<float>(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<float>(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;
}
}
}