// Copyright Epic Games, Inc. All Rights Reserved. #include "DSP/Envelope.h" #include "DSP/Dsp.h" namespace Audio { FEnvelope::FEnvelope() : VoiceId(0) , CurrentEnvelopeValue(0.0f) , CurrentEnvelopeBiasValue(0.0f) , SampleRate(44100.0f) , AttackTimeMSec(100.0f) , DecayTimeMsec(100.0f) , SustainGain(0.7f) , ReleaseTimeMsec(2000.0f) , ShutdownTimeMsec(10.0f) , ShutdownDelta(0.0f) , Depth(1.0f) , BiasDepth(1.0f) , CurrentState(EEnvelopeState::Off) , ModMatrix(nullptr) , bIsSimulatingAnalog(true) , bIsLegatoMode(false) , bIsRetriggerMode(false) , bChanged(true) , bInvert(false) , bBiasInvert(false) { } FEnvelope::~FEnvelope() { } void FEnvelope::Init(const float InSampleRate, const int32 InVoiceId, FModulationMatrix* InModMatrix, const bool bInSimulateAnalog) { VoiceId = InVoiceId; SampleRate = InSampleRate; SetSimulateAnalog(bInSimulateAnalog); bChanged = true; ModMatrix = InModMatrix; if (ModMatrix) { EnvSource = ModMatrix->CreatePatchSource(VoiceId); BiasedEnvSource = ModMatrix->CreatePatchSource(VoiceId); #if MOD_MATRIX_DEBUG_NAMES EnvSource.Name = TEXT("EnvSource"); BiasedEnvSource.Name = TEXT("BiasedEnvSource"); #endif } } FEnvelope::EEnvelopeState FEnvelope::GetState() const { return CurrentState; } void FEnvelope::SetSimulateAnalog(const bool bInSimulatingAnalog) { bIsSimulatingAnalog = bInSimulatingAnalog; bChanged = true; } void FEnvelope::Start() { bCurrentCycleIsADOnly = SustainGain <= SMALL_NUMBER; // Don't reset the envelope if we're in legato mode and we're not in release or off if (bIsLegatoMode && CurrentState != EEnvelopeState::Off && CurrentState != EEnvelopeState::Release) { return; } // Reset the envelope data Reset(); // Set the state back to attack no matter where it is CurrentState = FEnvelope::EEnvelopeState::Attack; } // logic for one mono note interrupting another mono note (same voice) void FEnvelope::StartLegato(const float InNewDepth) { // Envelope is not being used. Don't do the work (and don't divide by zero) if (Depth <= SMALL_NUMBER && InNewDepth <= SMALL_NUMBER) { return; } bCurrentCycleIsADOnly = SustainGain <= SMALL_NUMBER; switch (CurrentState) { case EEnvelopeState::Attack: { if (InNewDepth > Depth) { CurrentEnvelopeValue *= Depth / InNewDepth; Depth = InNewDepth; bChanged = true; } } break; case EEnvelopeState::Decay: { if (InNewDepth > Depth * CurrentEnvelopeValue) { CurrentState = EEnvelopeState::Attack; } CurrentEnvelopeValue *= Depth / InNewDepth; Depth = InNewDepth; bChanged = true; } break; case EEnvelopeState::Sustain: { // new sustain gain is higher if (InNewDepth > Depth * SustainGain) { CurrentEnvelopeValue *= Depth / InNewDepth; Depth = InNewDepth; CurrentState = EEnvelopeState::Attack; } bChanged = true; } break; case EEnvelopeState::Release: { // "attack up to" a larger new depth or "decay down to" a lower new depth CurrentState = EEnvelopeState::Attack; if(InNewDepth < Depth * CurrentEnvelopeValue) { CurrentState = EEnvelopeState::Decay; } CurrentEnvelopeValue *= Depth / InNewDepth; Depth = InNewDepth; bChanged = true; } break; default: { // previous behavior Depth = InNewDepth; Start(); } } } void FEnvelope::Stop() { if (CurrentEnvelopeValue == 0.0f) { // already finished (jump to off) CurrentState = EEnvelopeState::Off; } else if (!bCurrentCycleIsADOnly) { // normal envelope mode (jump to release) CurrentState = EEnvelopeState::Release; } else if (CurrentState == EEnvelopeState::Attack) { // AD only envelope mode (jump to decay) CurrentState = EEnvelopeState::Decay; } } void FEnvelope::Shutdown() { if (bIsLegatoMode) { return; } // If we're forcing off or if we're already off, then set state to off if (CurrentEnvelopeValue == 0.0f) { CurrentState = EEnvelopeState::Off; } else { // If we actually have an envelope value now, go to release CurrentState = EEnvelopeState::Shutdown; ShutdownDelta = -(1000.0f * CurrentEnvelopeValue) / ShutdownTimeMsec / SampleRate; } } void FEnvelope::Kill() { CurrentState = EEnvelopeState::Off; } bool FEnvelope::IsDone() const { return CurrentState == EEnvelopeState::Off; } void FEnvelope::Reset() { // Set the envelope state to off when reset CurrentState = EEnvelopeState::Off; // Recompute the envelopes if needed SetSimulateAnalog(bIsSimulatingAnalog); bChanged = true; // If set to reset the envelope value to 0.0, set the envelope back to 0 // Otherwise the envelope will continue to the target value from where it currently is if (bIsRetriggerMode) { CurrentEnvelopeValue = 0.0f; } } void FEnvelope::Update() { if (bChanged) { bChanged = false; if (bIsSimulatingAnalog) { // If in analog mode, we're going to emulate capacitor charging // Q = 1 - e^(-t/RC) for charging (attack) // Q = e^(-t/RC) for discharging AttackData.TCO = FMath::Exp(-1.5f); DecayData.TCO = FMath::Exp(-4.95f); } else { AttackData.TCO = 0.99999f; DecayData.TCO = FMath::Exp(-11.05f); } ReleaseData.TCO = DecayData.TCO; AttackData.TimeSamples = 0.001f * SampleRate * AttackTimeMSec;; DecayData.TimeSamples = 0.001f * SampleRate * DecayTimeMsec; ReleaseData.TimeSamples = 0.001f * SampleRate * ReleaseTimeMsec; AttackData.Coefficient = FMath::Exp(-FMath::Loge((1.0f + AttackData.TCO) / AttackData.TCO) / AttackData.TimeSamples); AttackData.Offset = (1.0f + AttackData.TCO) * (1.0f - AttackData.Coefficient); DecayData.Coefficient = FMath::Exp(-FMath::Loge((1.0f + DecayData.TCO) / DecayData.TCO) / DecayData.TimeSamples); DecayData.Offset = (bCurrentCycleIsADOnly? 0.0f : SustainGain - DecayData.TCO)*(1.0f - DecayData.Coefficient); ReleaseData.Coefficient = FMath::Exp(-FMath::Loge((1.0f + ReleaseData.TCO) / ReleaseData.TCO) / ReleaseData.TimeSamples); ReleaseData.Offset = -ReleaseData.TCO*(1.0f - ReleaseData.Coefficient); } } float FEnvelope::Generate(float* BiasedOutput) { // Update the envelope if it changed Update(); // Evaluate the finite state machine switch (CurrentState) { case EEnvelopeState::Off: { if (bIsRetriggerMode) { CurrentEnvelopeValue = 0.0f; } } break; case EEnvelopeState::Attack: { CurrentEnvelopeValue = AttackData.Offset + CurrentEnvelopeValue * AttackData.Coefficient; if (CurrentEnvelopeValue >= 1.0f || AttackTimeMSec <= 0.0f) { CurrentEnvelopeValue = 1.0f; CurrentState = EEnvelopeState::Decay; break; } } break; case EEnvelopeState::Decay: { // --- render value CurrentEnvelopeValue = DecayData.Offset + CurrentEnvelopeValue * DecayData.Coefficient; if ((CurrentEnvelopeValue <= SustainGain) || DecayTimeMsec <= 0.0f) { if(!bCurrentCycleIsADOnly) { CurrentEnvelopeValue = SustainGain; CurrentState = EEnvelopeState::Sustain; break; } else if (CurrentEnvelopeValue <= SMALL_NUMBER) { CurrentState = EEnvelopeState::Off; } } } break; case EEnvelopeState::Sustain: { // live-update sustain level (to hear changes made during sustain phase) CurrentEnvelopeValue = SustainGain; if (bCurrentCycleIsADOnly && SustainGain <= SMALL_NUMBER) { // Check if envelope was being used as AD only CurrentState = EEnvelopeState::Off; } } break; case EEnvelopeState::Release: { CurrentEnvelopeValue = ReleaseData.Offset + CurrentEnvelopeValue * ReleaseData.Coefficient; if (CurrentEnvelopeValue <= 0.0f || ReleaseTimeMsec <= 0.0f || SustainGain <= SMALL_NUMBER) { CurrentEnvelopeValue = 0.0f; CurrentState = EEnvelopeState::Off; break; } } break; case EEnvelopeState::Shutdown: { if (bIsRetriggerMode) { CurrentEnvelopeValue += ShutdownDelta; if (CurrentEnvelopeValue <= 0) { CurrentState = EEnvelopeState::Off; CurrentEnvelopeValue = 0.0f; break; } } else { CurrentState = EEnvelopeState::Off; } } break; } // Send the bias output (i.e. scale envelope by offset by sustain gain) float CurrentBiasedOutput = bBiasInvert ? 1.0f - CurrentEnvelopeValue : CurrentEnvelopeValue; CurrentBiasedOutput -= SustainGain; CurrentBiasedOutput *= BiasDepth; float OutputEnvValue = bInvert ? 1.0f - CurrentEnvelopeValue : CurrentEnvelopeValue; OutputEnvValue *= Depth; if (BiasedOutput) { *BiasedOutput = CurrentBiasedOutput; } if (ModMatrix) { ModMatrix->SetSourceValue(VoiceId, EnvSource, OutputEnvValue); ModMatrix->SetSourceValue(VoiceId, BiasedEnvSource, CurrentBiasedOutput); } return OutputEnvValue; } void FEnvelope::SetAttackTime(const float InAttackTimeMsec) { bChanged |= !FMath::IsNearlyEqual(AttackTimeMSec, InAttackTimeMsec); AttackTimeMSec = InAttackTimeMsec; } void FEnvelope::SetDecayTime(const float InDecayTimeMsec) { bChanged |= !FMath::IsNearlyEqual(DecayTimeMsec, InDecayTimeMsec); DecayTimeMsec = InDecayTimeMsec; } void FEnvelope::SetSustainGain(const float InSustainGain) { bChanged |= !FMath::IsNearlyEqual(SustainGain, InSustainGain); SustainGain = InSustainGain; } void FEnvelope::SetReleaseTime(const float InReleaseTimeMsec) { bChanged |= !FMath::IsNearlyEqual(ReleaseTimeMsec, InReleaseTimeMsec); ReleaseTimeMsec = InReleaseTimeMsec; } void FEnvelope::SetInvert(const bool bInInvert) { bInvert = bInInvert; } void FEnvelope::SetBiasInvert(const bool bInBiasInvert) { bBiasInvert = bInBiasInvert; } void FEnvelope::SetDepth(const float InDepth) { Depth = InDepth; } void FEnvelope::SetBiasDepth(const float InDepth) { BiasDepth = InDepth; } void FADEnvelope::Init(int32 InSampleRate) { SampleRate = (float)InSampleRate; SetAttackTimeSeconds(AttackTimeSeconds); SetDecayTimeSeconds(DecayTimeSeconds); } void FADEnvelope::SetAttackTimeSeconds(float InAttackTimeSeconds) { AttackTimeSeconds = FMath::Max(0.0f, InAttackTimeSeconds); AttackSampleCount = SampleRate * AttackTimeSeconds; } void FADEnvelope::SetDecayTimeSeconds(float InDecayTimeSeconds) { DecayTimeSeconds = FMath::Max(0.0f, InDecayTimeSeconds); DecaySampleCount = SampleRate * DecayTimeSeconds; } void FADEnvelope::SetAttackCurveFactor(float InAttackCurveFactor) { AttackCurveFactor = FMath::Max(KINDA_SMALL_NUMBER, AttackCurveFactor); } void FADEnvelope::SetDecayCurveFactor(float InDecayCurveFactor) { DecayCurveFactor = FMath::Max(KINDA_SMALL_NUMBER, InDecayCurveFactor); } void FADEnvelope::Attack() { // Resets the envelope state CurrentSampleIndex = 0; StartingEnvelopeValue = CurrentEnvelopeValue; } void FADEnvelope::GetNextEnvelopeOut(int32 StartFrame, int32 EndFrame, TArray& OutFinishedFrames, Audio::AlignedFloatBuffer& OutEnvelope) { if (CurrentSampleIndex == INDEX_NONE) { return; } float* OutEnvPtr = OutEnvelope.GetData(); int32 FrameIndex = StartFrame; for (int32 i = StartFrame; i < EndFrame; ++i) { // In attack if (CurrentSampleIndex <= AttackSampleCount) { float EnvelopeLeft = 1.0f - StartingEnvelopeValue; float AttackFraction = (float)CurrentSampleIndex++ / AttackSampleCount; CurrentEnvelopeValue = StartingEnvelopeValue + EnvelopeLeft * FMath::Pow(AttackFraction, AttackCurveFactor); OutEnvPtr[i] = CurrentEnvelopeValue; } else { int32 TotalSampleCount = AttackSampleCount + DecaySampleCount; // In decay if (CurrentSampleIndex < TotalSampleCount) { int32 SampleCountInDecayState = CurrentSampleIndex++ - AttackSampleCount; float DecayFraction = (float)SampleCountInDecayState / DecaySampleCount; CurrentEnvelopeValue = FMath::Pow(DecayFraction, DecayCurveFactor); OutEnvPtr[i] = CurrentEnvelopeValue; } // We're looping, so we reset the envelope state and continue on, which will go into attack phase else if (bIsLooping) { Attack(); // We still want to render this frame of audio for the restart envelope. Our AD envelope always starts from 0.0, so we know the first sample will be 0.0. OutEnvPtr[i] = 0.0f; // Still want to increment the sample index to avoid off-by one error with AD timing CurrentSampleIndex++; // Output that we "finished" this envelope. This may result in multiple entries if the AD loop repeats faster than the render block. OutFinishedFrames.Add(i); } // we're done else { int32 NumSamplesLeft = EndFrame - i; if (NumSamplesLeft > 0) { FMemory::Memzero(&OutEnvPtr[i], sizeof(float)*NumSamplesLeft); } CurrentSampleIndex = INDEX_NONE; OutFinishedFrames.Add(i); break; } } } } bool FADEnvelope::GetNextEnvelopeOut(float& OutEnvelope) { TArray OutFinishedFrames; GetNextEnvelopeOut(0, 1, OutFinishedFrames, OutEnvelope); return OutFinishedFrames.Num() > 0; } void FADEnvelope::GetNextEnvelopeOut(int32 StartFrame, int32 EndFrame, TArray& OutFinishedFrames, float& OutEnvelope) { // Don't need to do anything if we're not generating the envelope at the top of the block since this is a block-rate envelope if (StartFrame > 0 || CurrentSampleIndex == INDEX_NONE) { OutEnvelope = 0.0f; return; } // We are in attack if (CurrentSampleIndex < AttackSampleCount) { float EnvelopeLeft = 1.0f - StartingEnvelopeValue; float AttackFraction = (float)CurrentSampleIndex++ / AttackSampleCount; CurrentEnvelopeValue = StartingEnvelopeValue + EnvelopeLeft * FMath::Pow(AttackFraction, AttackCurveFactor); OutEnvelope = CurrentEnvelopeValue; } else { int32 TotalEnvSampleCount = (AttackSampleCount + DecaySampleCount); // We are in Decay if (CurrentSampleIndex < TotalEnvSampleCount) { int32 SampleCountInDecayState = CurrentSampleIndex++ - AttackSampleCount; float DecayFraction = (float)SampleCountInDecayState / DecaySampleCount; CurrentEnvelopeValue = 1.0f - FMath::Pow(DecayFraction, DecayCurveFactor); OutEnvelope = CurrentEnvelopeValue; } // We are looping so reset the sample index else if (bIsLooping) { CurrentSampleIndex = 0; OutFinishedFrames.Add(0); } else { // Envelope is done CurrentSampleIndex = INDEX_NONE; OutEnvelope = 0.0f; OutFinishedFrames.Add(0); } } } }