You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rb phil.popp #jira none #preflight 61e73bed1479b607fad4fad0 #ROBOMERGE-AUTHOR: miles.flanagan #ROBOMERGE-SOURCE: CL 18649412 in //UE5/Release-5.0/... via CL 18649851 via CL 18650098 #ROBOMERGE-BOT: UE5 (Release-Engine-Test -> Main) (v900-18638592) [CL 18650341 by miles flanagan in ue5-main branch]
519 lines
15 KiB
C++
519 lines
15 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DSP/DynamicsProcessor.h"
|
|
#include "SignalProcessingModule.h"
|
|
|
|
namespace Audio
|
|
{
|
|
FDynamicsProcessor::FDynamicsProcessor()
|
|
: ProcessingMode(EDynamicsProcessingMode::Compressor)
|
|
, EnvelopeFollowerPeakMode(EPeakMode::Peak)
|
|
, LookaheedDelayMsec(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)
|
|
, LinkMode(EDynamicsProcessorChannelLinkMode::Disabled)
|
|
, bIsAnalogMode(true)
|
|
, bKeyAuditionEnabled(false)
|
|
, bKeyHighshelfEnabled(false)
|
|
, bKeyLowshelfEnabled(false)
|
|
{
|
|
// The knee will have 2 points
|
|
KneePoints.Init(FVector2D(), 2);
|
|
}
|
|
|
|
FDynamicsProcessor::~FDynamicsProcessor()
|
|
{
|
|
}
|
|
|
|
void FDynamicsProcessor::Init(const float InSampleRate, const int32 InNumChannels)
|
|
{
|
|
SampleRate = InSampleRate;
|
|
|
|
SetNumChannels(InNumChannels);
|
|
SetKeyNumChannels(InNumChannels);
|
|
|
|
LookaheadDelay.Reset();
|
|
LookaheadDelay.AddDefaulted(InNumChannels);
|
|
|
|
for (int32 Channel = 0; Channel < InNumChannels; ++Channel)
|
|
{
|
|
LookaheadDelay[Channel].Init(SampleRate, 0.1f);
|
|
LookaheadDelay[Channel].SetDelayMsec(LookaheedDelayMsec);
|
|
|
|
EnvFollower[Channel].Init(FInlineEnvelopeFollowerInitParams{SampleRate, AttackTimeMsec, ReleaseTimeMsec, EnvelopeFollowerPeakMode, bIsAnalogMode});
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void FDynamicsProcessor::SetLookaheadMsec(const float InLookAheadMsec)
|
|
{
|
|
LookaheedDelayMsec = InLookAheadMsec;
|
|
for (int32 Channel = 0; Channel < LookaheadDelay.Num(); ++Channel)
|
|
{
|
|
LookaheadDelay[Channel].SetDelayMsec(LookaheedDelayMsec);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
void FDynamicsProcessor::SetRatio(const float InCompressionRatio)
|
|
{
|
|
// Don't let the compression ratio be 0.0!
|
|
Ratio = FMath::Max(InCompressionRatio, SMALL_NUMBER);
|
|
}
|
|
|
|
void FDynamicsProcessor::SetKneeBandwidth(const float InKneeBandwidthDb)
|
|
{
|
|
HalfKneeBandwidthDb = 0.5f * InKneeBandwidthDb;
|
|
}
|
|
|
|
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())
|
|
{
|
|
LookaheadDelay.Reset();
|
|
LookaheadDelay.AddDefaulted(InNumChannels);
|
|
|
|
for (int32 Channel = 0; Channel < InNumChannels; ++Channel)
|
|
{
|
|
LookaheadDelay[Channel].Init(SampleRate, 0.1f);
|
|
LookaheadDelay[Channel].SetDelayMsec(LookaheedDelayMsec);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
check(nullptr != InBuffer);
|
|
check(nullptr != OutBuffer);
|
|
|
|
const int32 NumChannels = GetNumChannels();
|
|
const int32 KeyNumChannels = GetKeyNumChannels();
|
|
|
|
if (InKeyBuffer)
|
|
{
|
|
int32 KeySampleIndex = 0;
|
|
for (int32 SampleIndex = 0; SampleIndex < InNumSamples; SampleIndex += NumChannels)
|
|
{
|
|
const float* KeyFrame = &InKeyBuffer[KeySampleIndex];
|
|
ProcessAudioFrame(&InBuffer[SampleIndex], &OutBuffer[SampleIndex], KeyFrame);
|
|
KeySampleIndex += KeyNumChannels;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int32 SampleIndex = 0; SampleIndex < InNumSamples; SampleIndex += NumChannels)
|
|
{
|
|
const float* KeyFrame = &InBuffer[SampleIndex];
|
|
ProcessAudioFrame(&InBuffer[SampleIndex], &OutBuffer[SampleIndex], KeyFrame);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FDynamicsProcessor::ProcessAudio(const float* InBuffer, const int32 InNumSamples, float* OutBuffer, const float* InKeyBuffer, float* OutEnvelope)
|
|
{
|
|
check(nullptr != InBuffer);
|
|
check(nullptr != OutBuffer);
|
|
check(nullptr != OutEnvelope);
|
|
|
|
const int32 NumChannels = GetNumChannels();
|
|
const int32 KeyNumChannels = GetKeyNumChannels();
|
|
|
|
|
|
if (InKeyBuffer)
|
|
{
|
|
int32 KeySampleIndex = 0;
|
|
for (int32 SampleIndex = 0; SampleIndex < InNumSamples; SampleIndex += NumChannels)
|
|
{
|
|
const float* KeyFrame = &InKeyBuffer[KeySampleIndex];
|
|
ProcessAudioFrame(&InBuffer[SampleIndex], &OutBuffer[SampleIndex], KeyFrame, &OutEnvelope[SampleIndex]);
|
|
KeySampleIndex += KeyNumChannels;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int32 SampleIndex = 0; SampleIndex < InNumSamples; SampleIndex += NumChannels)
|
|
{
|
|
const float* KeyFrame = &InBuffer[SampleIndex];
|
|
ProcessAudioFrame(&InBuffer[SampleIndex], &OutBuffer[SampleIndex], KeyFrame, &OutEnvelope[SampleIndex]);
|
|
}
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
ArrayClampInPlace(DetectorOuts, 0.f, 1.f);
|
|
|
|
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)
|
|
{
|
|
float 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;
|
|
}
|
|
|
|
// If we are in the range of compression
|
|
if (IsInProcessingThreshold(InEnvFollowerDb))
|
|
{
|
|
// 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;
|
|
|
|
// The knee calculation adjusts the slope to use via lagrangian interpolation through the slope
|
|
SlopeFactor = LagrangianInterpolation(KneePoints, InEnvFollowerDb);
|
|
}
|
|
|
|
float OutputGainDb = SlopeFactor * (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);
|
|
}
|
|
}
|