You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
- Add missing exponential factor to Modulation generator - Fix bounds restriction calculation for expontential factor - Add LFO ResetPhase implementation to override initial location in oscilator on rising edge where necessary (for modulation LFO, user range limited to unipolar but hardly ever wants to start at midpoint). - Add bipolar option to avoid oneshot/bipolar conflation for certain types of LFOs - Add back implementation for 5.1 deprecated function to avoid breaking licensees #rb phil.popp [REVIEW] phil.popp #jira UE-127886 #preflight 6346eed9b757820b7ae99b76 [CL 22483928 by rob gay in ue5-main branch]
384 lines
8.1 KiB
C++
384 lines
8.1 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DSP/Osc.h"
|
|
#include "DSP/Dsp.h"
|
|
|
|
namespace Audio
|
|
{
|
|
IOscBase::IOscBase()
|
|
: VoiceId(0)
|
|
, SampleRate(44100.0f)
|
|
, Nyquist(0.5f * SampleRate)
|
|
, Freq(440.0f)
|
|
, BaseFreq(1.0f)
|
|
, Gain(1.0f)
|
|
, ExternalGainMod(1.0f)
|
|
, Phase(0.0f)
|
|
, PhaseInc(0.0f)
|
|
, PulseWidthBase(0.5f)
|
|
, PulseWidthMod(0.0f)
|
|
, PulseWidth(0.0f)
|
|
, ModMatrix(nullptr)
|
|
, FollowerOsc(nullptr)
|
|
, bIsPlaying(false)
|
|
, bChanged(false)
|
|
{
|
|
}
|
|
|
|
IOscBase::~IOscBase()
|
|
{
|
|
}
|
|
|
|
void IOscBase::Init(const float InSampleRate, const int32 InVoiceId, FModulationMatrix* InMatrix, const int32 ModMatrixStage)
|
|
{
|
|
VoiceId = InVoiceId;
|
|
SetSampleRate(InSampleRate);
|
|
|
|
bChanged = true;
|
|
|
|
// Set up the patch destinations for the mod matrix if we've been given a mod matrix
|
|
ModMatrix = InMatrix;
|
|
if (ModMatrix)
|
|
{
|
|
ModFrequencyDest = ModMatrix->CreatePatchDestination(VoiceId, ModMatrixStage, 50.0f);
|
|
ModPulseWidthDest = ModMatrix->CreatePatchDestination(VoiceId, ModMatrixStage, 1.0f);
|
|
ModGainDest = ModMatrix->CreatePatchDestination(VoiceId, ModMatrixStage, 1.0f);
|
|
ModAddDest = ModMatrix->CreatePatchDestination(VoiceId, ModMatrixStage, 50.0f);
|
|
ModScaleDest = ModMatrix->CreatePatchDestination(VoiceId, ModMatrixStage, 1.0f);
|
|
|
|
#if MOD_MATRIX_DEBUG_NAMES
|
|
ModFrequencyDest.Name = TEXT("ModFrequencyDest");
|
|
ModPulseWidthDest.Name = TEXT("ModPulseWidthDest");
|
|
ModGainDest.Name = TEXT("ModGainDest");
|
|
ModAddDest.Name = TEXT("ModAddDest");
|
|
ModScaleDest.Name = TEXT("ModScaleDest");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetFrequency(const float InFreqBase)
|
|
{
|
|
if (InFreqBase != BaseFreq)
|
|
{
|
|
BaseFreq = InFreqBase;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetFrequencyMod(const float InFreqMod)
|
|
{
|
|
if (InFreqMod != FreqData.ExternalMod)
|
|
{
|
|
FreqData.ExternalMod = InFreqMod;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetNote(const float InNote)
|
|
{
|
|
const float MidiFreq = GetFrequencyFromMidi(InNote);
|
|
SetFrequency(MidiFreq);
|
|
}
|
|
|
|
void IOscBase::SetCents(const float InCents)
|
|
{
|
|
if (FreqData.Cents != InCents)
|
|
{
|
|
FreqData.Cents = InCents;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetOctave(const float InOctave)
|
|
{
|
|
if (FreqData.Octave != InOctave)
|
|
{
|
|
FreqData.Octave = InOctave;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetSampleRate(const float InSampleRate)
|
|
{
|
|
if (InSampleRate != SampleRate)
|
|
{
|
|
SampleRate = InSampleRate;
|
|
Nyquist = 0.5f * SampleRate;
|
|
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetSemitones(const float InSemiTone)
|
|
{
|
|
if (FreqData.Semitones != InSemiTone)
|
|
{
|
|
FreqData.Semitones = InSemiTone;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetDetune(const float InDetune)
|
|
{
|
|
if (FreqData.Detune != InDetune)
|
|
{
|
|
FreqData.Detune = InDetune;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetPitchBend(const float InPitchBend)
|
|
{
|
|
if (FreqData.PitchBend != InPitchBend)
|
|
{
|
|
FreqData.PitchBend = InPitchBend;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::SetFreqScale(const float InFreqScale)
|
|
{
|
|
if (FreqData.Scale != InFreqScale)
|
|
{
|
|
FreqData.Scale = InFreqScale;
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::Update()
|
|
{
|
|
// Compute the final output frequency
|
|
|
|
if (ModMatrix)
|
|
{
|
|
bChanged |= ModMatrix->GetDestinationValue(VoiceId, ModFrequencyDest, FreqData.Mod);
|
|
bChanged |= ModMatrix->GetDestinationValue(VoiceId, ModPulseWidthDest, PulseWidthMod);
|
|
}
|
|
|
|
if (bChanged)
|
|
{
|
|
bChanged = false;
|
|
|
|
float FreqModSum = FreqData.Mod + FreqData.ExternalMod + FreqData.Detune + FreqData.PitchBend + 12.0f * FreqData.Octave + FreqData.Semitones + 0.01f * FreqData.Cents;
|
|
float PulseWidthSum = PulseWidthBase + PulseWidthMod;
|
|
|
|
PulseWidth = FMath::Clamp(PulseWidthSum, 0.02f, 0.98f);
|
|
Freq = BaseFreq * FreqData.Scale * GetFrequencyMultiplier(FreqModSum);
|
|
Freq = FMath::Clamp(Freq, -Nyquist, Nyquist);
|
|
|
|
// Update the phase increment
|
|
PhaseInc = Freq / SampleRate;
|
|
}
|
|
|
|
}
|
|
|
|
void IOscBase::SetPulseWidth(const float InPulseWidth)
|
|
{
|
|
if (InPulseWidth != PulseWidthBase)
|
|
{
|
|
PulseWidthBase = FMath::Clamp(InPulseWidth, 0.0f, 1.0f);
|
|
bChanged = true;
|
|
}
|
|
}
|
|
|
|
void IOscBase::ResetPhase()
|
|
{
|
|
Phase = 0.0f;
|
|
}
|
|
|
|
void IOscBase::SetSlaveOsc(IOscBase* InSlaveOsc)
|
|
{
|
|
FollowerOsc = InSlaveOsc;
|
|
}
|
|
|
|
void IOscBase::SetFollowerOsc(IOscBase* InFollowerOsc)
|
|
{
|
|
FollowerOsc = InFollowerOsc;
|
|
}
|
|
|
|
void IOscBase::Reset()
|
|
{
|
|
Phase = 0.0f;
|
|
ExternalGainMod = 1.0f;
|
|
PulseWidthMod = 0.0f;
|
|
FreqData.PitchBend = 0.0f;
|
|
FreqData.Mod = 0.0f;
|
|
FreqData.ExternalMod = 0.0f;
|
|
FreqData.Detune = 0.0f;
|
|
bChanged = true;
|
|
}
|
|
|
|
FOsc::FOsc()
|
|
: TriangleSign(-1.0f)
|
|
, DPW_z1(0.0f)
|
|
, PulseWidthLerped(0.5f)
|
|
, OscType(EOsc::Sine)
|
|
{
|
|
}
|
|
|
|
FOsc::~FOsc()
|
|
{
|
|
}
|
|
|
|
void FOsc::Start()
|
|
{
|
|
Reset();
|
|
bIsPlaying = true;
|
|
Update();
|
|
}
|
|
|
|
void FOsc::Stop()
|
|
{
|
|
bIsPlaying = false;
|
|
}
|
|
|
|
void FOsc::Reset()
|
|
{
|
|
IOscBase::Reset();
|
|
|
|
// For these types our phase starts at 0.5
|
|
if (OscType == EOsc::Saw || OscType == EOsc::Triangle)
|
|
{
|
|
Phase = 0.5f;
|
|
}
|
|
|
|
TriangleSign = -1.0f;
|
|
DPW_z1 = 0.0f;
|
|
}
|
|
|
|
void FOsc::Update()
|
|
{
|
|
IOscBase::Update();
|
|
|
|
PulseWidthLerped = PulseWidth;
|
|
}
|
|
|
|
float FOsc::Generate(float* AuxOutput)
|
|
{
|
|
if (!bIsPlaying)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
float Output = 0.0f;
|
|
const bool bWrapped = WrapPhase();
|
|
|
|
switch (OscType)
|
|
{
|
|
case EOsc::Sine:
|
|
{
|
|
const float Radians = 2.0f * Phase * PI - PI;
|
|
Output = FastSin3(-1.0f * Radians);
|
|
}
|
|
break;
|
|
|
|
case EOsc::Saw:
|
|
{
|
|
// Two-sided wave-shaped sawtooth
|
|
static const float A = FastTanh(1.5f);
|
|
Output = GetBipolar(Phase);
|
|
Output = FastTanh(1.5f * Output) / A;
|
|
Output += PolySmooth(Phase, PhaseInc);
|
|
}
|
|
break;
|
|
|
|
case EOsc::Square:
|
|
{
|
|
// First generate a smoothed sawtooth
|
|
float SquareSaw1 = GetBipolar(Phase);
|
|
SquareSaw1 += PolySmooth(Phase, PhaseInc);
|
|
|
|
float CurrentPulseWidth = PulseWidthLerped.GetNextValue();
|
|
|
|
// Create a second sawtooth that is phase-shifted based on the pulsewidth
|
|
float NewPhase = 0.0f;
|
|
if (PhaseInc > 0.0f)
|
|
{
|
|
NewPhase = Phase + CurrentPulseWidth;
|
|
if (NewPhase >= 1.0f)
|
|
{
|
|
NewPhase -= 1.0f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
NewPhase = Phase - CurrentPulseWidth;
|
|
if (NewPhase <= 0.0f)
|
|
{
|
|
NewPhase += 1.0f;
|
|
}
|
|
}
|
|
|
|
float SquareSaw2 = GetBipolar(NewPhase);
|
|
SquareSaw2 += PolySmooth(NewPhase, PhaseInc);
|
|
|
|
// Subtract 2 saws, then apply DC correction
|
|
// Simplified version of
|
|
// float Output = 0.5f * SquareSaw1 - 0.5f * SquareSaw2;
|
|
// Output = 2.0f * (Output + CurrentPulseWidth) - 1.0f;
|
|
return SquareSaw1 - SquareSaw2 + 2.0f * (CurrentPulseWidth - 0.5f);
|
|
}
|
|
break;
|
|
|
|
case EOsc::Triangle:
|
|
{
|
|
// Square a simple saw wave, differentiate (add prev sample)
|
|
// Then scale by a
|
|
if (bWrapped)
|
|
{
|
|
// Flip the sign of the square mod
|
|
TriangleSign *= -1.0f;
|
|
}
|
|
|
|
// Get a saw wave
|
|
const float Saw = GetBipolar(Phase);
|
|
const float SawSquaredInvMod = (1.0f - Saw * Saw) * TriangleSign;
|
|
|
|
// Perform differentiation by subtracting current squared saw
|
|
const float Differentiated = SawSquaredInvMod - DPW_z1;
|
|
DPW_z1 = SawSquaredInvMod;
|
|
Output = Differentiated * SampleRate / (4.0f*Freq*(1.0f - PhaseInc));
|
|
|
|
UpdatePhase();
|
|
}
|
|
break;
|
|
|
|
case EOsc::Noise:
|
|
Output = Noise.Generate();
|
|
break;
|
|
}
|
|
|
|
// Update the LFO phase after computing LFO values
|
|
UpdatePhase();
|
|
|
|
// Apply the final matrix-mod gain
|
|
return Output * Gain * ExternalGainMod;
|
|
}
|
|
|
|
float FOsc::PolySmooth(const float InPhase, const float InPhaseInc)
|
|
{
|
|
// Smooth out the edges of the saw based on its current frequency
|
|
// using a polynomial to smooth it at the discontinuity. This
|
|
// limits aliasing by avoiding the infinite frequency at the discontinuity.
|
|
|
|
float Output = 0.0f;
|
|
|
|
// The current phase is on the left side of discontinuity
|
|
if (InPhase > 1.0f - InPhaseInc)
|
|
{
|
|
const float Dist = (InPhase - 1.0f) / InPhaseInc;
|
|
Output = -Dist*Dist - 2.0f * Dist - 1.0f;
|
|
}
|
|
// The current phase is on the right side of the discontinuity
|
|
else if (InPhase < InPhaseInc)
|
|
{
|
|
// Distance into polynomial
|
|
const float Dist = InPhase / InPhaseInc;
|
|
Output = Dist*Dist - 2.0f * Dist + 1.0f;
|
|
}
|
|
|
|
return Output;
|
|
}
|
|
|
|
}
|