// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/FloatArrayMath.h" #include "DSP/SineWaveTableOsc.h" #include "MetasoundFacade.h" #include "Internationalization/Text.h" #include "MetasoundAudioBuffer.h" #include "MetasoundExecutableOperator.h" #include "MetasoundNodeRegistrationMacro.h" #include "MetasoundParamHelper.h" #include "MetasoundPrimitives.h" #include "MetasoundStandardNodesCategories.h" #include "MetasoundStandardNodesNames.h" #define LOCTEXT_NAMESPACE "MetasoundAdditiveSynthNode" namespace Metasound { namespace AdditiveSynthVertexNames { METASOUND_PARAM(BaseFrequency, "Base Frequency", "Sinusoid frequency that harmonics are based on, clamped to 0.0 - Nyquist."); METASOUND_PARAM(HarmonicMultipliers, "HarmonicMultipliers", "Array of float harmonic multipliers that will be applied to the base frequency. Number of sinusoids used depends on the size of this array. Clamped from 0.0 to a max such that the resulting frequency won't go above Nyquist."); METASOUND_PARAM(Amplitudes, "Amplitudes", "Array of sinusoid amplitudes, clamped to 0.0 - 1.0"); METASOUND_PARAM(Phases, "Phases", "Array of sinusoid phases in degrees, clamped to 0.0 - 360.0"); METASOUND_PARAM(PanAmounts, "Pan Amounts", "Array of pan amounts (using equal power law; -1.0 is full left, 1.0 is full right)."); METASOUND_PARAM(LeftAudioOut, "Out Left Audio", "Left output synthesized audio."); METASOUND_PARAM(RightAudioOut, "Out Right Audio", "Right output synthesized audio."); } class FAdditiveSynthOperator : public TExecutableOperator { public: using FInputFloatArrayReadRef = TDataReadReference>; using FInputFloatArrayType = TArray; static const FNodeClassMetadata& GetNodeInfo() { auto InitNodeInfo = []() -> FNodeClassMetadata { const FName OperatorName = "Additive Synth"; const FText NodeDisplayName = METASOUND_LOCTEXT("Metasound_AdditiveSynthNodeDisplayName", "Additive Synth"); const FText NodeDescription = METASOUND_LOCTEXT("Metasound_AdditiveSynthNodeDescription", "Synthesizes audio output given input array of sinusoids."); FNodeClassMetadata Info; Info.ClassName = { Metasound::StandardNodes::Namespace, OperatorName, FName() }; Info.MajorVersion = 1; Info.MinorVersion = 0; Info.DisplayName = NodeDisplayName; Info.Description = NodeDescription; Info.Author = PluginAuthor; Info.PromptIfMissing = PluginNodeMissingPrompt; Info.DefaultInterface = GetVertexInterface(); Info.CategoryHierarchy.Emplace(NodeCategories::Generators); Info.Keywords = { METASOUND_LOCTEXT("AdditiveSynthesisKeyword", "Synthesis") }; return Info; }; static const FNodeClassMetadata Info = InitNodeInfo(); return Info; } static const FVertexInterface& GetVertexInterface() { using namespace AdditiveSynthVertexNames; static const FVertexInterface Interface( FInputVertexInterface( TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(BaseFrequency), 440.0f), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(HarmonicMultipliers)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(Amplitudes)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(Phases)), TInputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(PanAmounts)) ), FOutputVertexInterface( TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(LeftAudioOut)), TOutputDataVertex(METASOUND_GET_PARAM_NAME_AND_METADATA(RightAudioOut)) ) ); return Interface; } static TUniquePtr CreateOperator(const FBuildOperatorParams& InParams, FBuildResults& OutResults) { using namespace AdditiveSynthVertexNames; const FInputVertexInterfaceData& InputData = InParams.InputData; FFloatReadRef InputBaseFrequency = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(BaseFrequency), InParams.OperatorSettings); FInputFloatArrayReadRef InputHarmonicMultipliers = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(HarmonicMultipliers), InParams.OperatorSettings); FInputFloatArrayReadRef InputAmplitudes = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(Amplitudes), InParams.OperatorSettings); FInputFloatArrayReadRef InputPhases = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(Phases), InParams.OperatorSettings); FInputFloatArrayReadRef InputPanAmounts = InputData.GetOrCreateDefaultDataReadReference(METASOUND_GET_PARAM_NAME(PanAmounts), InParams.OperatorSettings); return MakeUnique(InParams.OperatorSettings, InputBaseFrequency, InputHarmonicMultipliers, InputAmplitudes, InputPhases, InputPanAmounts); } FAdditiveSynthOperator(const FOperatorSettings& InSettings, const FFloatReadRef& InputBaseFrequency, const FInputFloatArrayReadRef& InputHarmonicMultipliers, const FInputFloatArrayReadRef& InputAmplitudes, const FInputFloatArrayReadRef& InputPhases, const FInputFloatArrayReadRef& InputPanAmounts) : BaseFrequency(InputBaseFrequency) , HarmonicMultipliers(InputHarmonicMultipliers) , Amplitudes(InputAmplitudes) , Phases(InputPhases) , PanAmounts(InputPanAmounts) , LeftAudioOut(FAudioBufferWriteRef::CreateNew(InSettings)) , RightAudioOut(FAudioBufferWriteRef::CreateNew(InSettings)) , SampleRate(InSettings.GetSampleRate()) { CreateSinusoids(); } virtual void BindInputs(FInputVertexInterfaceData& InOutVertexData) override { using namespace AdditiveSynthVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(BaseFrequency), BaseFrequency); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(HarmonicMultipliers), HarmonicMultipliers); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(Amplitudes), Amplitudes); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(Phases), Phases); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(PanAmounts), PanAmounts); } virtual void BindOutputs(FOutputVertexInterfaceData& InOutVertexData) override { using namespace AdditiveSynthVertexNames; InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(LeftAudioOut), LeftAudioOut); InOutVertexData.BindReadVertex(METASOUND_GET_PARAM_NAME(RightAudioOut), RightAudioOut); } void Reset(const IOperator::FResetParams& InParams) { CreateSinusoids(); LeftAudioOut->Zero(); RightAudioOut->Zero(); } void Execute() { int32 NumSamples = LeftAudioOut->Num(); // Zero out output buffers since we'll sum directly into them FMemory::Memzero(LeftAudioOut->GetData(), sizeof(float) * NumSamples); FMemory::Memzero(RightAudioOut->GetData(), sizeof(float) * NumSamples); // Clear temporary buffers if (ScratchBuffer.Num() != NumSamples) { ScratchBuffer.Reset(); ScratchBuffer.Init(0, NumSamples); } int32 NumSinusoids = HarmonicMultipliers->Num(); // Number of elements in parallel arrays doesn't match the current number of oscillators if (NumSinusoids != SinusoidData.Num()) { CreateSinusoids(); } // Flag to update frequency (which depends on node base frequency and individual harmonic multiplier) if necessary bool UpdateFrequency = false; float Frequency = *BaseFrequency; // Clamp frequency to Nyquist const float Nyquist = SampleRate / 2.0f; Frequency = FMath::Clamp(Frequency, 0.0f, Nyquist); if (!FMath::IsNearlyEqual(Frequency, PreviousBaseFrequency)) { PreviousBaseFrequency = Frequency; UpdateFrequency = true; } // For each sinusoid, update parameters and generate audio for (int32 SinusoidIndex = 0; SinusoidIndex < NumSinusoids; ++SinusoidIndex) { Audio::FSineWaveTableOsc& WaveTableOsc = SinusoidData[SinusoidIndex]; // Update harmonic multiplier and resulting frequency if necessary float HarmonicMultiplier = (*HarmonicMultipliers)[SinusoidIndex]; // Maximum is Nyquist / Frequency because the actual frequency will be HarmonicMultiplier * Frequency HarmonicMultiplier = FMath::Clamp(HarmonicMultiplier, 0.0f, Nyquist / Frequency); if (!FMath::IsNearlyEqual(HarmonicMultiplier, PreviousHarmonicMultipliers[SinusoidIndex])) { PreviousHarmonicMultipliers[SinusoidIndex] = HarmonicMultiplier; UpdateFrequency = true; } if (UpdateFrequency) { WaveTableOsc.SetFrequencyHz(Frequency * HarmonicMultiplier); } // Update phase if necessary, set to 0 if no phases are given // If the number of elements in Phases doesn't equal the number of sinusoids, // modulus on the Phases array (i.e. if 2 elements, phases will alternate) float Phase = 0.0f; if (Phases->Num() == 0) { if (!FMath::IsNearlyEqual(Phase, PreviousPhases[SinusoidIndex])) { PreviousPhases[SinusoidIndex] = Phase; WaveTableOsc.SetPhase(Phase); } } else { int32 PhaseModIndex = SinusoidIndex % Phases->Num(); Phase = FMath::Clamp((*Phases)[PhaseModIndex], 0.0f, 360.0f); if (!FMath::IsNearlyEqual(Phase, PreviousPhases[PhaseModIndex])) { PreviousPhases[PhaseModIndex] = Phase; WaveTableOsc.SetPhase(Phase / 360.0f); } } // Get samples from wavetable WaveTableOsc.Generate(ScratchBuffer.GetData(), NumSamples); // Apply pan float LeftPanGain = 1.0f; float RightPanGain = 1.0f; // If PanAmounts is empty, will default to 0 // If the number of elements in PanAmounts doesn't equal the number of sinusoids, // modulus on the PanAmounts array (i.e. if 2 elements, pan amounts will alternate) if (PanAmounts->Num() > 0) { float Fraction = 0.5f * (FMath::Clamp((*PanAmounts)[SinusoidIndex % PanAmounts->Num()], -1.0f, 1.0f) + 1.0f); // Using equal power panning law FMath::SinCos(&RightPanGain, &LeftPanGain, 0.5f * PI * Fraction); } // Clamp given amplitude float BaseGain = 1.0f; if (Amplitudes->Num() > 0) { BaseGain = FMath::Clamp((*Amplitudes)[SinusoidIndex % Amplitudes->Num()], 0.0f, 1.0f); } // Multiply by gain (both amplitude and pan) float NewLeftGain = LeftPanGain * BaseGain; float NewRightGain = RightPanGain * BaseGain; // Mix into output buffer TArrayView ScratchBufferView(ScratchBuffer.GetData(), NumSamples); TArrayView LeftAudioOutView(LeftAudioOut->GetData(), NumSamples); TArrayView RightAudioOutView(RightAudioOut->GetData(), NumSamples); Audio::ArrayMixIn(ScratchBufferView, LeftAudioOutView, NewLeftGain); Audio::ArrayMixIn(ScratchBufferView, RightAudioOutView, NewRightGain); } } private: void CreateSinusoids() { int32 NumSinusoids = HarmonicMultipliers->Num(); SinusoidData.Reset(NumSinusoids); PreviousHarmonicMultipliers.Reset(NumSinusoids); PreviousPhases.Reset(NumSinusoids); // Cache information so we don't have to update the oscillator every time, pad to NumSinusoids PreviousHarmonicMultipliers = *HarmonicMultipliers; PreviousPhases = *Phases; if (PreviousPhases.Num() < NumSinusoids) { PreviousPhases.AddZeroed(NumSinusoids - PreviousPhases.Num()); } PreviousBaseFrequency = *BaseFrequency; for (int32 SinusoidIndex = 0; SinusoidIndex < NumSinusoids; ++SinusoidIndex) { // Create wavetable oscillators for each sinusoid Audio::FSineWaveTableOsc WaveTableOsc = Audio::FSineWaveTableOsc(); float Phase = Phases->Num() > 0 ? (*Phases)[SinusoidIndex % Phases->Num()] : 0.0f; WaveTableOsc.Init(SampleRate, (*HarmonicMultipliers)[SinusoidIndex] * (*BaseFrequency), Phase / 360.0f); SinusoidData.Add(WaveTableOsc); } } TArray SinusoidData; FFloatReadRef BaseFrequency; FInputFloatArrayReadRef HarmonicMultipliers; FInputFloatArrayReadRef Amplitudes; FInputFloatArrayReadRef Phases; FInputFloatArrayReadRef PanAmounts; // Cached copies of sinusoid parameters to avoid unnecessary updates TArray PreviousHarmonicMultipliers; TArray PreviousPhases; float PreviousBaseFrequency = 440.0f; // Scratch aligned buffer Audio::FAlignedFloatBuffer ScratchBuffer; // Audio output FAudioBufferWriteRef LeftAudioOut; FAudioBufferWriteRef RightAudioOut; // Sample rate, will be set in constructor float SampleRate = 48000.0f; }; using FAdditiveSynthNode = TNodeFacade; METASOUND_REGISTER_NODE(FAdditiveSynthNode) } #undef LOCTEXT_NAMESPACE