Files
UnrealEngine/Engine/Plugins/Runtime/Metasound/Source/MetasoundStandardNodes/Private/MetasoundLimiterNode.cpp
2025-05-18 13:04:45 +08:00

256 lines
8.8 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Internationalization/Text.h"
#include "MetasoundExecutableOperator.h"
#include "MetasoundEnumRegistrationMacro.h"
#include "MetasoundNodeRegistrationMacro.h"
#include "MetasoundPrimitives.h"
#include "MetasoundStandardNodesNames.h"
#include "MetasoundAudioBuffer.h"
#include "MetasoundStandardNodesCategories.h"
#include "MetasoundFacade.h"
#include "MetasoundParamHelper.h"
#include "DSP/Dsp.h"
#include "DSP/DynamicsProcessor.h"
#define LOCTEXT_NAMESPACE "MetasoundStandardNodes_LimiterNode"
namespace Metasound
{
/* Mid-Side Encoder */
namespace LimiterVertexNames
{
METASOUND_PARAM(InputAudio, "Audio", "Incoming audio signal to compress.");
METASOUND_PARAM(InputInGainDb, "Input Gain dB", "Gain to apply to the input before limiting, in decibels. Maximum 100 dB. ");
METASOUND_PARAM(InputThresholdDb, "Threshold dB", "Amplitude threshold above which gain will be reduced.");
METASOUND_PARAM(InputReleaseTime, "Release Time", "How long it takes for audio below the threshold to return to its original volume level.");
METASOUND_PARAM(InputKneeMode, "Knee", "Whether the limiter uses a hard or soft knee.");
METASOUND_PARAM(OutputAudio, "Audio", "The output audio signal.");
}
enum class EKneeMode
{
Hard = 0,
Soft,
};
DECLARE_METASOUND_ENUM(EKneeMode, EKneeMode::Hard, METASOUNDSTANDARDNODES_API,
FEnumKneeMode, FEnumKneeModeInfo, FKneeModeReadRef, FEnumKneeModeWriteRef);
DEFINE_METASOUND_ENUM_BEGIN(EKneeMode, FEnumKneeMode, "KneeMode")
DEFINE_METASOUND_ENUM_ENTRY(EKneeMode::Hard, "KneeModeHardDescription", "Hard", "KneeModeHardDescriptionTT", "Only audio strictly above the threshold is affected by the limiter."),
DEFINE_METASOUND_ENUM_ENTRY(EKneeMode::Soft, "KneeModeSoftDescription", "Soft", "KneeModeSoftDescriptionTT", "Limiter activates more smoothly near the threshold."),
DEFINE_METASOUND_ENUM_END()
// Operator Class
class FLimiterOperator : public TExecutableOperator<FLimiterOperator>
{
public:
static constexpr float HardKneeBandwitdh = 0.0f;
static constexpr float SoftKneeBandwitdh = 10.0f;
static constexpr float MaxInputGain = 100.0f;
FLimiterOperator(const FBuildOperatorParams& InParams,
const FAudioBufferReadRef& InAudio,
const FFloatReadRef& InGainDb,
const FFloatReadRef& InThresholdDb,
const FTimeReadRef& InReleaseTime,
const FKneeModeReadRef& InKneeMode)
: AudioInput(InAudio)
, InGainDbInput(InGainDb)
, ThresholdDbInput(InThresholdDb)
, ReleaseTimeInput(InReleaseTime)
, KneeModeInput(InKneeMode)
, AudioOutput(FAudioBufferWriteRef::CreateNew(InParams.OperatorSettings))
, Limiter()
, PrevInGainDb(*InGainDb)
, PrevThresholdDb(*InThresholdDb)
, PrevReleaseTime(FMath::Max(FTime::ToMilliseconds(*InReleaseTime), 0.0))
{
Reset(InParams);
}
static const FNodeClassMetadata& GetNodeInfo()
{
auto CreateNodeClassMetadata = []() -> FNodeClassMetadata
{
FVertexInterface NodeInterface = DeclareVertexInterface();
FNodeClassMetadata Metadata
{
FNodeClassName{StandardNodes::Namespace, TEXT("Limiter"), StandardNodes::AudioVariant},
1, // Major Version
0, // Minor Version
METASOUND_LOCTEXT("LimiterDisplayName", "Limiter"),
METASOUND_LOCTEXT("LimiterDesc", "Prevents a signal from going above a given threshold."),
PluginAuthor,
PluginNodeMissingPrompt,
NodeInterface,
{NodeCategories::Dynamics},
{},
FNodeDisplayStyle{}
};
return Metadata;
};
static const FNodeClassMetadata Metadata = CreateNodeClassMetadata();
return Metadata;
}
static const FVertexInterface& DeclareVertexInterface()
{
using namespace LimiterVertexNames;
static const FVertexInterface Interface(
FInputVertexInterface(
TInputDataVertex<FAudioBuffer>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputAudio)),
TInputDataVertex<float>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputInGainDb), 0.0f),
TInputDataVertex<float>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputThresholdDb), 0.0f),
TInputDataVertex<FTime>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputReleaseTime), 0.1f),
TInputDataVertex<FEnumKneeMode>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputKneeMode), (int32)EKneeMode::Hard)
),
FOutputVertexInterface(
TOutputDataVertex<FAudioBuffer>(METASOUND_GET_PARAM_NAME_AND_METADATA(OutputAudio))
)
);
return Interface;
}
virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override
{
using namespace LimiterVertexNames;
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputAudio), AudioInput);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputInGainDb), InGainDbInput);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputThresholdDb), ThresholdDbInput);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputReleaseTime), ReleaseTimeInput);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputKneeMode), KneeModeInput);
}
virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override
{
using namespace LimiterVertexNames;
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(OutputAudio), AudioOutput);
}
static TUniquePtr<IOperator> CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults)
{
using namespace LimiterVertexNames;
const FInputVertexInterfaceData& InputData = InParams.InputData;
FAudioBufferReadRef AudioIn = InputData.GetOrCreateDefaultDataReadReference<FAudioBuffer>(METASOUND_GET_PARAM_NAME(InputAudio), InParams.OperatorSettings);
FFloatReadRef InGainDbIn = InputData.GetOrCreateDefaultDataReadReference<float>(METASOUND_GET_PARAM_NAME(InputInGainDb), InParams.OperatorSettings);
FFloatReadRef ThresholdDbIn = InputData.GetOrCreateDefaultDataReadReference<float>(METASOUND_GET_PARAM_NAME(InputThresholdDb), InParams.OperatorSettings);
FTimeReadRef ReleaseTimeIn = InputData.GetOrCreateDefaultDataReadReference<FTime>(METASOUND_GET_PARAM_NAME(InputReleaseTime), InParams.OperatorSettings);
FKneeModeReadRef KneeModeIn = InputData.GetOrCreateDefaultDataReadReference<FEnumKneeMode>(METASOUND_GET_PARAM_NAME(InputKneeMode), InParams.OperatorSettings);
return MakeUnique<FLimiterOperator>(InParams, AudioIn, InGainDbIn, ThresholdDbIn, ReleaseTimeIn, KneeModeIn);
}
void Reset(const IOperator::FResetParams& InParams)
{
AudioOutput->Zero();
const float ClampedInGainDb = FMath::Min(*InGainDbInput, MaxInputGain);
const float ClampedReleaseTime = FMath::Max(FTime::ToMilliseconds(*ReleaseTimeInput), 0.0);
Limiter.Init(InParams.OperatorSettings.GetSampleRate(), 1);
Limiter.SetProcessingMode(Audio::EDynamicsProcessingMode::Limiter);
Limiter.SetInputGain(ClampedInGainDb);
Limiter.SetThreshold(*ThresholdDbInput);
Limiter.SetAttackTime(0.0f);
Limiter.SetReleaseTime(ClampedReleaseTime);
Limiter.SetPeakMode(Audio::EPeakMode::Peak);
switch (*KneeModeInput)
{
default:
case EKneeMode::Hard:
Limiter.SetKneeBandwidth(HardKneeBandwitdh);
break;
case EKneeMode::Soft:
Limiter.SetKneeBandwidth(SoftKneeBandwitdh);
break;
}
PrevInGainDb = ClampedInGainDb;
PrevReleaseTime = ClampedReleaseTime;
PrevThresholdDb = *ThresholdDbInput;
PrevKneeMode = *KneeModeInput;
}
void Execute()
{
/* Update parameters */
float ClampedInGainDb = FMath::Min(*InGainDbInput, MaxInputGain);
if (!FMath::IsNearlyEqual(ClampedInGainDb, PrevInGainDb))
{
Limiter.SetInputGain(ClampedInGainDb);
PrevInGainDb = ClampedInGainDb;
}
if (!FMath::IsNearlyEqual(*ThresholdDbInput, PrevThresholdDb))
{
Limiter.SetThreshold(*ThresholdDbInput);
PrevThresholdDb = *ThresholdDbInput;
}
// Release time cannot be negative
double CurrRelease = FMath::Max(FTime::ToMilliseconds(*ReleaseTimeInput), 0.0f);
if (!FMath::IsNearlyEqual(CurrRelease, PrevReleaseTime))
{
Limiter.SetReleaseTime(CurrRelease);
PrevReleaseTime = CurrRelease;
}
if (PrevKneeMode != *KneeModeInput)
{
switch (*KneeModeInput)
{
default:
case EKneeMode::Hard:
Limiter.SetKneeBandwidth(HardKneeBandwitdh);
break;
case EKneeMode::Soft:
Limiter.SetKneeBandwidth(SoftKneeBandwitdh);
break;
}
PrevKneeMode = *KneeModeInput;
}
Limiter.ProcessAudio(AudioInput->GetData(), AudioInput->Num(), AudioOutput->GetData());
}
private:
FAudioBufferReadRef AudioInput;
FFloatReadRef InGainDbInput;
FFloatReadRef ThresholdDbInput;
FTimeReadRef ReleaseTimeInput;
FKneeModeReadRef KneeModeInput;
FAudioBufferWriteRef AudioOutput;
// Internal DSP Limiter
Audio::FDynamicsProcessor Limiter;
// Cached variables
float PrevInGainDb;
float PrevThresholdDb;
double PrevReleaseTime;
EKneeMode PrevKneeMode;
};
// Node Class
using FLimiterNode = TNodeFacade<FLimiterOperator>;
// Register node
METASOUND_REGISTER_NODE(FLimiterNode)
}
#undef LOCTEXT_NAMESPACE