215 lines
6.9 KiB
C++
215 lines
6.9 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DSP/GrainDelay.h"
|
|
|
|
namespace Audio
|
|
{
|
|
namespace GrainDelay
|
|
{
|
|
// TODO: consider making this a construction pin or cvar
|
|
static constexpr int32 GrainEnvelopeSampleCount = 1024;
|
|
static constexpr float MaxAbsPitchShiftInOctaves = 6.0f;
|
|
|
|
FGrainDelay::FGrainDelay(const float InSampleRate, const float InMaxDelaySeconds)
|
|
: SampleRate(InSampleRate)
|
|
, MaxDelaySeconds(FMath::Max(InMaxDelaySeconds, 0.0f))
|
|
{
|
|
// DelayLine must accommodate the delay of the grain and the pitch shifter.
|
|
// The pitch shifter has a maximum delay and requires an extra millisecond
|
|
// for phase offsets. An additional sample is added to account for floating
|
|
// point rounding.
|
|
|
|
const float RoundingErrorBufferInSeconds = 1.f / InSampleRate;
|
|
const float MaxTapDelayInSeconds = (FTapDelayPitchShifter::MaxDelayLength + 1.f) / 1000.f;
|
|
const float DelayLineSizeInSeconds = MaxDelaySeconds + MaxTapDelayInSeconds + RoundingErrorBufferInSeconds;
|
|
GrainDelayLine.Init(SampleRate, DelayLineSizeInSeconds);
|
|
|
|
// Generate the grain data
|
|
GenerateEnvelopeData(GrainEnvelope, GrainEnvelopeSampleCount, GrainEnvelopeType);
|
|
|
|
InitDynamicsProcessor();
|
|
}
|
|
|
|
FGrainDelay::~FGrainDelay()
|
|
{
|
|
}
|
|
|
|
void FGrainDelay::Reset()
|
|
{
|
|
GrainDelayLine.Reset();
|
|
|
|
InitDynamicsProcessor();
|
|
|
|
// Move any active grains into free grains
|
|
FreeGrains.Append(ActiveGrains);
|
|
ActiveGrains.Reset();
|
|
}
|
|
|
|
float FGrainDelay::GetGrainDelayClamped(const float InDelay) const
|
|
{
|
|
return FMath::Clamp(InDelay, 0.0f, 1000.0f * MaxDelaySeconds);
|
|
}
|
|
|
|
float FGrainDelay::GetGrainDurationClamped(const float InDuration) const
|
|
{
|
|
return FMath::Clamp(InDuration, 0.0f, 1000.0f * MaxDelaySeconds);
|
|
}
|
|
|
|
float FGrainDelay::GetGrainDelayRatioClamped(const float InGrainDelayRatio) const
|
|
{
|
|
return FMath::Clamp(InGrainDelayRatio, -1.0f, 1.0f);
|
|
}
|
|
|
|
float FGrainDelay::GetGrainPitchShiftClamped(const float InPitchShift) const
|
|
{
|
|
return FMath::Clamp(InPitchShift, -12.0f * MaxAbsPitchShiftInOctaves, 12.0f * MaxAbsPitchShiftInOctaves);
|
|
}
|
|
|
|
float FGrainDelay::GetGrainPitchShiftFrameRatio(const float InPitchShift) const
|
|
{
|
|
return FMath::Pow(2.0f, InPitchShift / 12.0f);
|
|
}
|
|
|
|
void FGrainDelay::SetMaxGrains(const int32 InMaxGrains)
|
|
{
|
|
MaxGrains = FMath::Clamp(InMaxGrains, 1, 100);
|
|
}
|
|
|
|
void FGrainDelay::SetGrainEnvelope(const Audio::Grain::EEnvelope InGrainEnvelope)
|
|
{
|
|
if (GrainEnvelopeType != InGrainEnvelope)
|
|
{
|
|
GrainEnvelopeType = InGrainEnvelope;
|
|
GenerateEnvelopeData(GrainEnvelope, 1024, GrainEnvelopeType);
|
|
}
|
|
}
|
|
|
|
void FGrainDelay::SetFeedbackAmount(float InFeedbackAmount)
|
|
{
|
|
FeedbackAmount = FMath::Clamp(InFeedbackAmount, 0.0f, 0.95f);
|
|
}
|
|
|
|
void FGrainDelay::SetGrainBasePitchShiftRatio(const float InPitchRatioBase)
|
|
{
|
|
if (!FMath::IsNearlyEqual(InPitchRatioBase, PitchShiftRatioBase))
|
|
{
|
|
PitchShiftRatioBase = InPitchRatioBase;
|
|
for (const int32 ActiveGrainId : ActiveGrains)
|
|
{
|
|
FGrain& Grain = GrainPool[ActiveGrainId];
|
|
|
|
// Set the pitch shift ratio of the grain.
|
|
// This function will take into account the initial random pitch offset
|
|
Grain.SetGrainPitchShiftRatio(InPitchRatioBase, SampleRate);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FGrainDelay::SpawnGrain(const float InDelay, const float InDuration, const float InPitchShiftRatioOffset)
|
|
{
|
|
if (ActiveGrains.Num() >= FMath::Max(MaxGrains, 1))
|
|
{
|
|
// Hit the max grain cap, no grain for you!
|
|
return;
|
|
}
|
|
|
|
// Find or create a new grain index
|
|
int32 NewGrainId = INDEX_NONE;
|
|
if (FreeGrains.Num() == 0)
|
|
{
|
|
NewGrainId = GrainPool.Num();
|
|
GrainPool.Add(FGrain());
|
|
}
|
|
else
|
|
{
|
|
NewGrainId = FreeGrains.Pop();
|
|
}
|
|
|
|
// Spawn a new grain and setup it's initial state and data
|
|
const float DurationSeconds = 0.001f * InDuration;
|
|
|
|
FGrain& NewGrain = GrainPool[NewGrainId];
|
|
NewGrain.NumFramesRendered = 0.0f;
|
|
NewGrain.DelayTapPositionMilliseconds = InDelay;
|
|
NewGrain.DurationFrames = FMath::Max(DurationSeconds * SampleRate, 1.0f);
|
|
NewGrain.PitchShiftRatioOffset = InPitchShiftRatioOffset;
|
|
NewGrain.PitchShifter.Init(SampleRate, 0.0f, 1000.0f * DurationSeconds);
|
|
NewGrain.SetGrainPitchShiftRatio(PitchShiftRatioBase, SampleRate);
|
|
|
|
// Add to the active grain list
|
|
ActiveGrains.Add(NewGrainId);
|
|
}
|
|
|
|
float FGrainDelay::SynthesizeFrame(const Audio::FDelay& InDelayLine)
|
|
{
|
|
float OutFrame = 0.0f;
|
|
|
|
// Loop through active grains from end so we can clean it up as we go when a grain is done
|
|
for (int32 ActiveGrainIndex = ActiveGrains.Num() - 1; ActiveGrainIndex >= 0; --ActiveGrainIndex)
|
|
{
|
|
// Get the grain data
|
|
const int32 GrainId = ActiveGrains[ActiveGrainIndex];
|
|
FGrain& Grain = GrainPool[GrainId];
|
|
|
|
// Calculate the grain envelope
|
|
// should have never been added to the pool
|
|
check(!FMath::IsNearlyZero(Grain.DurationFrames));
|
|
const float Fraction = FMath::Min(Grain.NumFramesRendered / Grain.DurationFrames, 1.0f);
|
|
const float GrainVolume = Grain::GetValue(GrainEnvelope, Fraction);
|
|
|
|
const float PitchShiftedSample = Grain.PitchShifter.ReadDopplerShiftedTapFromDelay(InDelayLine, Grain.DelayTapPositionMilliseconds);
|
|
OutFrame += GrainVolume * PitchShiftedSample;
|
|
|
|
// Note we are tracking real frames rendered not frames consumed from the delay for grain duration
|
|
Grain.NumFramesRendered += 1.0f;
|
|
|
|
// Clean up the grain once it's done
|
|
if (Grain.NumFramesRendered >= Grain.DurationFrames)
|
|
{
|
|
// Pop back on this id so we can reuse it next time a grain is spawned
|
|
FreeGrains.Add(GrainId);
|
|
|
|
// Remove at swap allows us to clean up the active grain list while we loop through it
|
|
ActiveGrains.RemoveAtSwap(ActiveGrainIndex, EAllowShrinking::No);
|
|
}
|
|
}
|
|
|
|
// Feed audio through the dynamics processor to prevent clipping.
|
|
DynamicsProcessor.ProcessAudio(&OutFrame,1, &OutFrame);
|
|
|
|
return OutFrame;
|
|
}
|
|
|
|
void FGrainDelay::SynthesizeAudio(const int32 StartFrame, const int32 EndFrame, const float* InAudioBuffer, float* OutAudioBuffer)
|
|
{
|
|
int32 NumFramesProcessed = 0;
|
|
for (int32 FrameIndex = StartFrame; FrameIndex < EndFrame + 1; ++FrameIndex)
|
|
{
|
|
++NumFramesProcessed;
|
|
GrainDelayLine.WriteDelayAndInc(InAudioBuffer[FrameIndex] + FeedbackAmount * LastFrame);
|
|
LastFrame = SynthesizeFrame(GrainDelayLine);
|
|
|
|
OutAudioBuffer[FrameIndex] = LastFrame;
|
|
}
|
|
}
|
|
|
|
void FGrainDelay::InitDynamicsProcessor()
|
|
{
|
|
// Setup the dynamics processor
|
|
DynamicsProcessor.Init(SampleRate, 1);
|
|
DynamicsProcessor.SetLookaheadMsec(3.0f);
|
|
DynamicsProcessor.SetAttackTime(0.0f);
|
|
DynamicsProcessor.SetReleaseTime(10.0f);
|
|
DynamicsProcessor.SetThreshold(-6.0f);
|
|
DynamicsProcessor.SetKneeBandwidth(3.0f);
|
|
DynamicsProcessor.SetInputGain(0.0f);
|
|
DynamicsProcessor.SetOutputGain(0.0f);
|
|
DynamicsProcessor.SetAnalogMode(false);
|
|
DynamicsProcessor.SetPeakMode(EPeakMode::Peak);
|
|
DynamicsProcessor.SetProcessingMode(EDynamicsProcessingMode::Limiter);
|
|
}
|
|
};
|
|
|
|
|
|
}
|