Files
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

537 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Internationalization/Text.h"
#include "MetasoundParamHelper.h"
#include "MetasoundDataReferenceCollection.h"
#include "MetasoundExecutableOperator.h"
#include "MetasoundFacade.h"
#include "MetasoundNodeInterface.h"
#include "MetasoundNodeRegistrationMacro.h"
#include "MetasoundOperatorInterface.h"
#include "MetasoundPrimitives.h"
#include "MetasoundSampleCounter.h"
#include "MetasoundStandardNodesCategories.h"
#include "MetasoundStandardNodesNames.h"
#include "DSP/EnvelopeFollower.h"
#include "DSP/Filter.h"
#define LOCTEXT_NAMESPACE "MetasoundStandardNodes_Subharmonizer"
namespace Metasound
{
namespace SubharmonizerVertexNames
{
METASOUND_PARAM(InputAudio, "Audio In", "Audio to process");
METASOUND_PARAM(InputFrequency, "Filter Frequency", "Cutoff Frequency for Lowpass Input and Output Filter to reduce noise, additional Highpass Filter at half frequency.");
METASOUND_PARAM(InputLinearGain, "Gain (Lin)", "Output Gain Scalar.")
METASOUND_PARAM(InputSubharmonic, "Subharmonic", "Which Subharmonic to output, input clamped between 1 and 6.");
METASOUND_PARAM(InputOctaverMode, "Octaver Mode", "When true, output is sub-octaves; when false, output is subharmonics.")
METASOUND_PARAM(OutputAudio, "Audio Out", "Processed audio signal");
}
namespace SubharmonizerNodePrivate
{
/*
* Subharmonizer processor. Supports suboctave mode and harmonic selection. Uses zero-crossing counting.
*/
struct FSubharmonizer
{
// Audio processor
void Process(const float* InBuffer, const int32 NumFrames, float* BufferOut);
// Parameter update
void Update(const int32 InSubharmonic, const bool bOctaverModeIn);
// Reset processor
void Reset();
private:
// Bitmask used to count subharmonics
uint8 SubharmonicBitmask = 2;
// Subharmonic base value for modulo op
uint8 SubharmonicBase = 4;
// Current subharmonic set for output
uint8 Subharmonic = 1;
// Tracking for sign state when in subharmonic mode
uint8 CurrentSignState = 0;
// Tracking most recent sign value
bool bLastSign = false;
// Whether or not to output sub-octaves instead of subharmonics
bool bOctaverMode = false;
// Tracking which side of zero we're currently on
bool bPolarity = false;
};
}
class FSubharmonizerOperator : public TExecutableOperator<FSubharmonizerOperator>
{
public:
static const FNodeClassMetadata& GetNodeInfo();
static const FVertexInterface& GetDefaultInterface();
static TUniquePtr<IOperator> CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults);
FSubharmonizerOperator(const FOperatorSettings& InSettings,
const FAudioBufferReadRef& InAudioIn,
const FFloatReadRef& InFrequencyIn,
const FFloatReadRef& InGainDBIn,
const FInt32ReadRef& InSubharmonicIn,
const FBoolReadRef& InOctaverModeIn
);
virtual ~FSubharmonizerOperator() = default;
virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override;
virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override;
void UpdateParams();
void Reset(const IOperator::FResetParams& InParams);
void Execute();
private:
// Internal reset function
void InternalReset();
// Input Pin Refs
FAudioBufferReadRef AudioIn;
FFloatReadRef FrequencyIn;
FFloatReadRef LinearGainIn;
FInt32ReadRef SubharmonicIn;
FBoolReadRef OctaverModeIn;
// Output Pin Ref
FAudioBufferWriteRef AudioOut;
// Environment data
float SampleRate = 48000.0f;
int32 NumFramesPerBlock = 0;
// Max filter frequency/nyquist frequency
float MaxFilterFrequency = 24000.f;
// Clamped Minimum frequency
const float MinFilterFrequency = 20.f;
// Cached Input
float PreviouisFilterFrequency = 120.f;
// Default cutoff frequency values
float LPFFilterFrequency = 120.f;
float HPFFilterFrequency = 60.f;
// Clamped minimum frequency
const float InputHighPassFilterFrequency = 30.f;
float LinearGain = 2.f;
float PreviousLinearGain = 2.f;
int32 InputSubharmonic = 1;
int32 PreviousInputSubharmonic = 1;
// Clamped ranges
const int32 MinInputSubharmonic = 1;
const int32 MaxInputSubharmonic = 6;
bool bOctaverMode = false;
// Processors
SubharmonizerNodePrivate::FSubharmonizer Subharmonizer;
// Input LPF: Removes unnecessary input noise
Audio::FBiquadFilter InputLowPassFilter;
// Output LPF: Smoothing filter for output from pulse wave generated by Subharmonizer
Audio::FBiquadFilter OutputLowPassFilter;
// Output HPF: Helps to remove low-frequency noise
Audio::FBiquadFilter OutputHighPassFilter;
// Envelope follower to help shape output according to input
Audio::FEnvelopeFollower EnvelopeFollower;
// Scratch Data
Audio::FAlignedFloatBuffer ScratchBufferA;
Audio::FAlignedFloatBuffer ScratchBufferB;
Audio::FAlignedFloatBuffer EnvelopeBuffer;
Audio::FAlignedFloatBuffer GainInterpolationBuffer;
};
const FVertexInterface& FSubharmonizerOperator::GetDefaultInterface()
{
using namespace SubharmonizerVertexNames;
auto CreateDefaultInterface = []() -> FVertexInterface
{
FInputVertexInterface InputInterface;
InputInterface.Add(TInputDataVertex<FAudioBuffer>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputAudio)));
InputInterface.Add(TInputDataVertex<float>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputFrequency), 80.0f));
InputInterface.Add(TInputDataVertex<float>(METASOUND_GET_PARAM_NAME_AND_METADATA(InputLinearGain), 1.f));
InputInterface.Add(TInputDataVertex<int32>(METASOUND_GET_PARAM_NAME_AND_METADATA_ADVANCED(InputSubharmonic), 1));
InputInterface.Add(TInputDataVertex<bool>(METASOUND_GET_PARAM_NAME_AND_METADATA_ADVANCED(InputOctaverMode), false));
FOutputVertexInterface OutputInterface;
OutputInterface.Add(TOutputDataVertex<FAudioBuffer>(METASOUND_GET_PARAM_NAME_AND_METADATA(OutputAudio)));
return FVertexInterface(InputInterface, OutputInterface);
};
static const FVertexInterface Interface = CreateDefaultInterface();
return Interface;
}
void SubharmonizerNodePrivate::FSubharmonizer::Process(const float* InBuffer, const int32 NumFrames, float* BufferOut)
{
// Process loop in Octaver Mode
if (bOctaverMode)
{
// Create mask for modulo op
const int32 SubharmonicBaseMask = SubharmonicBase - 1;
// Process creates a 1.f or -1.f output value based on either flipping bits or using the results of a modulus operator
for (int32 i = 0; i < NumFrames; ++i)
{
// The sign from this sample
const bool bNewSign = InBuffer[i] > 0.f;
// Is the sign is different, we increment our "counters"
if (bLastSign != bNewSign)
{
// Update sign state based on counter
++CurrentSignState;
CurrentSignState = CurrentSignState & SubharmonicBaseMask;
}
// Select correct output value based on sign and bitmask state
BufferOut[i] = ((CurrentSignState & SubharmonicBitmask) ? 1.f : -1.f);
// Cache sign value for next sample evaluation
bLastSign = bNewSign;
}
}
// Process loop when not in Octaver Mode
else
{
// Process creates a 1.f or -1.f output value based on either flipping bits or using the results of a modulus operator
for (int32 i = 0; i < NumFrames; ++i)
{
// The sign from this sample
const bool bNewSign = InBuffer[i] > 0.f;
// Is the sign is different, we increment our "counters"
if (bLastSign != bNewSign)
{
// Update sign state based on counter
CurrentSignState = FMath::Modulo(++CurrentSignState, SubharmonicBase);
// If not using octaver mode, base sign change on returning to 0 after mod op
if (CurrentSignState == 0)
{
bPolarity = !bPolarity;
}
}
// Select correct output value based on polarity
BufferOut[i] = bPolarity ? 1.f : -1.f;
// Cache sign value for next sample evaluation
bLastSign = bNewSign;
}
}
}
void SubharmonizerNodePrivate::FSubharmonizer::Update(const int32 InSubharmonic, const bool bOctaverModeIn)
{
// Cache updated values
Subharmonic = InSubharmonic;
bOctaverMode = bOctaverModeIn;
// Set bitmask
SubharmonicBitmask = 1 << Subharmonic;
// Adjust Base based on octaver mode or not
if(bOctaverMode)
{
SubharmonicBase = 1 << FMath::Min(Subharmonic + 1, 7);
}
else
{
SubharmonicBase = FMath::Min(Subharmonic + 1, 7);
}
}
void SubharmonizerNodePrivate::FSubharmonizer::Reset()
{
// Revert to starting values
Subharmonic = 2;
bLastSign = false;
CurrentSignState = 0;
bOctaverMode = false;
}
const FNodeClassMetadata& FSubharmonizerOperator::GetNodeInfo()
{
auto InitNodeInfo = []() -> FNodeClassMetadata
{
FName OperatorName = *FString::Printf(TEXT("Subharmonizer"));
FText NodeDisplayName = METASOUND_LOCTEXT("SubharmonizerDisplayNamePattern", "Subharmonizer");
const FText NodeDescription = METASOUND_LOCTEXT("SubharmonizerDescription", "Generates a subharmonic of the input audio signal.");
TArray<const FText> NodeKeywords
{
METASOUND_LOCTEXT("SubharmonizerKeyword_Subharmonic", "Subharmonic"),
METASOUND_LOCTEXT("SubharmonizerKeyword_Octaver", "Octaver"),
METASOUND_LOCTEXT("SubharmonizerKeyword_Bass", "Bass")
};
FNodeClassMetadata Info;
Info.ClassName = { StandardNodes::Namespace, OperatorName, TEXT("") };
Info.MajorVersion = 1;
Info.MinorVersion = 0;
Info.DisplayName = NodeDisplayName;
Info.Description = NodeDescription;
Info.Author = PluginAuthor;
Info.PromptIfMissing = PluginNodeMissingPrompt;
Info.DefaultInterface = GetDefaultInterface();
Info.CategoryHierarchy.Emplace(NodeCategories::Generators);
Info.Keywords.Append(NodeKeywords);
return Info;
};
static const FNodeClassMetadata Info = InitNodeInfo();
return Info;
}
TUniquePtr<IOperator> FSubharmonizerOperator::CreateOperator(const FBuildOperatorParams& InParams,
FBuildResults& OutResults)
{
using namespace SubharmonizerVertexNames;
const FInputVertexInterfaceData& InputData = InParams.InputData;
FAudioBufferReadRef AudioIn = InputData.GetOrCreateDefaultDataReadReference<FAudioBuffer>(METASOUND_GET_PARAM_NAME(InputAudio), InParams.OperatorSettings);
FFloatReadRef FrequencyIn = InputData.GetOrCreateDefaultDataReadReference<float>(METASOUND_GET_PARAM_NAME(InputFrequency), InParams.OperatorSettings);
FFloatReadRef LinearGainIn = InputData.GetOrCreateDefaultDataReadReference<float>(METASOUND_GET_PARAM_NAME(InputLinearGain), InParams.OperatorSettings);
FInt32ReadRef SubharmonicIn = InputData.GetOrCreateDefaultDataReadReference<int32>(METASOUND_GET_PARAM_NAME(InputSubharmonic), InParams.OperatorSettings);
FBoolReadRef OctaverModeIn = InputData.GetOrCreateDefaultDataReadReference<bool>(METASOUND_GET_PARAM_NAME(InputOctaverMode), InParams.OperatorSettings);
return MakeUnique<FSubharmonizerOperator>(InParams.OperatorSettings,
AudioIn,
FrequencyIn,
LinearGainIn,
SubharmonicIn,
OctaverModeIn
);
}
FSubharmonizerOperator::FSubharmonizerOperator(const FOperatorSettings& InSettings,
const FAudioBufferReadRef& InAudioIn,
const FFloatReadRef& InFrequencyIn,
const FFloatReadRef& InLinearGainIn,
const FInt32ReadRef& InSubharmonicIn,
const FBoolReadRef& InOctaverModeIn
)
: AudioIn(InAudioIn)
, FrequencyIn(InFrequencyIn)
, LinearGainIn(InLinearGainIn)
, SubharmonicIn(InSubharmonicIn)
, OctaverModeIn(InOctaverModeIn)
, AudioOut(TDataWriteReferenceFactory<FAudioBuffer>::CreateExplicitArgs(InSettings))
{
NumFramesPerBlock = InSettings.GetNumFramesPerBlock();
SampleRate = InSettings.GetSampleRate();
InternalReset();
}
void FSubharmonizerOperator::BindInputs(FInputVertexInterfaceData& InOutVertexData)
{
using namespace SubharmonizerVertexNames;
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputAudio), AudioIn);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputFrequency), FrequencyIn);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputLinearGain), LinearGainIn);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputSubharmonic), SubharmonicIn);
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(InputOctaverMode), OctaverModeIn);
}
void FSubharmonizerOperator::BindOutputs(FOutputVertexInterfaceData& InOutVertexData)
{
using namespace SubharmonizerVertexNames;
InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(OutputAudio), AudioOut);
}
void FSubharmonizerOperator::UpdateParams()
{
// Cache latest input values
LPFFilterFrequency = FMath::Clamp(*FrequencyIn, MinFilterFrequency, MaxFilterFrequency);
HPFFilterFrequency = FMath::Max(MinFilterFrequency, LPFFilterFrequency * 0.5);
InputSubharmonic = FMath::Clamp(*SubharmonicIn,MinInputSubharmonic, MaxInputSubharmonic);
LinearGain = FMath::Max(*LinearGainIn, 0.f);
// Update Frequency values
if(!FMath::IsNearlyEqual(PreviouisFilterFrequency, LPFFilterFrequency))
{
InputLowPassFilter.SetFrequency(LPFFilterFrequency);
OutputLowPassFilter.SetFrequency(LPFFilterFrequency / FMath::Pow(2.f, *SubharmonicIn));
OutputHighPassFilter.SetFrequency(HPFFilterFrequency / FMath::Pow(2.f, *SubharmonicIn));
PreviouisFilterFrequency = LPFFilterFrequency;
}
// Update gain value
if(!FMath::IsNearlyEqual(LinearGain, *LinearGainIn))
{
LinearGain = FMath::Max(*LinearGainIn, 0.f);
}
// Update subharmonics and mode
if(PreviousInputSubharmonic != InputSubharmonic || bOctaverMode != *OctaverModeIn)
{
bOctaverMode = *OctaverModeIn;
Subharmonizer.Update(InputSubharmonic, bOctaverMode);
// Adjust internal frequency values to target new spectral output range
if(bOctaverMode)
{
OutputLowPassFilter.SetFrequency(LPFFilterFrequency / FMath::Pow(2.f, InputSubharmonic));
OutputHighPassFilter.SetFrequency(HPFFilterFrequency / FMath::Pow(2.f, InputSubharmonic));
}
else
{
OutputLowPassFilter.SetFrequency(LPFFilterFrequency / FMath::Max(InputSubharmonic, 1));
OutputLowPassFilter.SetFrequency(LPFFilterFrequency / FMath::Max(InputSubharmonic + 1, 1));
}
// Cache for tracking
PreviousInputSubharmonic = InputSubharmonic;
}
}
void FSubharmonizerOperator::Reset(const IOperator::FResetParams& InParams)
{
// Cache environment
NumFramesPerBlock = InParams.OperatorSettings.GetNumFramesPerBlock();
SampleRate = InParams.OperatorSettings.GetSampleRate();
InternalReset();
}
void FSubharmonizerOperator::InternalReset()
{
// Max filter frequency/nyquist frequency
MaxFilterFrequency = SampleRate * 0.5;
// Init scratch buffers
ScratchBufferA.Init(0.f,NumFramesPerBlock);
ScratchBufferB.Init(0.f,NumFramesPerBlock);
EnvelopeBuffer.Init(0.f, NumFramesPerBlock);
GainInterpolationBuffer.Init(0.f, NumFramesPerBlock);
// Constrain input values
LinearGain = FMath::Max(*LinearGainIn, 0.f);
// Cache to Previous
PreviousLinearGain = LinearGain;
// Reset processor
Subharmonizer.Reset();
// Clamp subharmonic input
InputSubharmonic = FMath::Clamp(*SubharmonicIn, MinInputSubharmonic, MaxInputSubharmonic);
PreviousInputSubharmonic = InputSubharmonic;
// Cache input
bOctaverMode = *OctaverModeIn;
Subharmonizer.Update(InputSubharmonic, bOctaverMode);
// Clamp input values
LPFFilterFrequency = FMath::Clamp(*FrequencyIn, MinFilterFrequency, MaxFilterFrequency);
// Set HPF (low noise filter) cutoff to half that of the the output LPF smoothing filter
HPFFilterFrequency = FMath::Max(MinFilterFrequency, LPFFilterFrequency * 0.5);
PreviouisFilterFrequency = LPFFilterFrequency;
// Init input (high frequency noise reduction) filter
InputLowPassFilter.Init(SampleRate, 1, Audio::EBiquadFilter::ButterworthLowPass, LPFFilterFrequency);
// Init output smoothing filter (smooths pulse wave output from subharmonizer)
OutputLowPassFilter.Init(SampleRate, 1, Audio::EBiquadFilter::ButterworthLowPass, LPFFilterFrequency / FMath::Pow(2.f, *SubharmonicIn));
// Init output high pass filter (reduces low frequency rumble/noise on output)
OutputHighPassFilter.Init(SampleRate, 1, Audio::EBiquadFilter::ButterworthHighPass, HPFFilterFrequency / FMath::Pow(2.f, *SubharmonicIn));
// Init output
AudioOut->Zero();
// Init envelope follower parameters
const Audio::FEnvelopeFollowerInitParams EnvelopeFollowerInitParams(
SampleRate,
1,
10.f,
100.f,
Audio::EPeakMode::Peak,
true,
300.f);
// Init envelope follower
EnvelopeFollower.Init(EnvelopeFollowerInitParams);
// Call update
UpdateParams();
}
void FSubharmonizerOperator::Execute()
{
UpdateParams();
const int32 Frames = AudioIn->Num();
// Calculate Envelope
EnvelopeFollower.ProcessAudio(AudioIn->GetData(), Frames, EnvelopeBuffer.GetData());
// Process Input Lowpass Filter
InputLowPassFilter.ProcessAudio(AudioIn->GetData(), Frames, ScratchBufferA.GetData());
// Process Subharmonizer
Subharmonizer.Process(ScratchBufferA.GetData(), Frames, ScratchBufferB.GetData());
// Process Output Smoothing Filter
OutputLowPassFilter.ProcessAudio(ScratchBufferB.GetData(), Frames, ScratchBufferA.GetData());
// Process Highpass Filter
OutputHighPassFilter.ProcessAudio(ScratchBufferA.GetData(), Frames, ScratchBufferB.GetData());
// Apply Envelope
Audio::ArrayMultiply(ScratchBufferB, EnvelopeBuffer, ScratchBufferA);
// Apply Linear Gain Interpolation
Audio::ArrayFade(ScratchBufferA, PreviousLinearGain, LinearGain, *AudioOut);
// Update previous gain
PreviousLinearGain = LinearGain;
}
// Node registration
using FSubharmonizerNode = TNodeFacade<FSubharmonizerOperator>;
METASOUND_REGISTER_NODE(FSubharmonizerNode);
} // namespace Metasound
#undef LOCTEXT_NAMESPACE