118 lines
4.1 KiB
C++
118 lines
4.1 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "DSP/TapDelayPitchShifter.h"
|
|
|
|
namespace Audio
|
|
{
|
|
namespace PitchShiftUtils
|
|
{
|
|
static float GetPitchShiftClamped(const float InPitchShift)
|
|
{
|
|
return FMath::Clamp(InPitchShift, -12.0f * FTapDelayPitchShifter::MaxAbsPitchShiftInOctaves, 12.0f * FTapDelayPitchShifter::MaxAbsPitchShiftInOctaves);
|
|
}
|
|
|
|
static float GetDelayLengthClamped(const float InBufferLength)
|
|
{
|
|
return FMath::Clamp(InBufferLength, FTapDelayPitchShifter::MinDelayLength, FTapDelayPitchShifter::MaxDelayLength);
|
|
}
|
|
}
|
|
|
|
FTapDelayPitchShifter::FTapDelayPitchShifter()
|
|
{
|
|
}
|
|
|
|
FTapDelayPitchShifter::~FTapDelayPitchShifter()
|
|
{
|
|
}
|
|
|
|
void FTapDelayPitchShifter::Init(const float InSampleRate, const float InPitchShift, const float InDelayLength)
|
|
{
|
|
SampleRate = InSampleRate;
|
|
CurrentTargetDelayLength = PitchShiftUtils::GetDelayLengthClamped(InDelayLength);
|
|
CurrentDelayLength.Init(CurrentTargetDelayLength);
|
|
CurrentPitchShift = PitchShiftUtils::GetPitchShiftClamped(InPitchShift);
|
|
CurrentPitchShiftRatio = Audio::GetFrequencyMultiplier(CurrentPitchShift);
|
|
|
|
UpdatePhasorPhaseIncrement();
|
|
}
|
|
|
|
void FTapDelayPitchShifter::UpdatePhasorPhaseIncrement()
|
|
{
|
|
const float CurrentLengthSecondsClamped = 0.001f * FMath::Max(CurrentDelayLength.PeekCurrentValue(), 1.0f);
|
|
const float PhasorFrequency = (1.0f - CurrentPitchShiftRatio) / CurrentLengthSecondsClamped;
|
|
PhasorPhaseIncrement = PhasorFrequency / SampleRate;
|
|
}
|
|
|
|
|
|
void FTapDelayPitchShifter::SetDelayLength(const float InDelayLength)
|
|
{
|
|
const float NewDelayLength = PitchShiftUtils::GetDelayLengthClamped(InDelayLength);
|
|
if (!FMath::IsNearlyEqual(NewDelayLength, CurrentTargetDelayLength))
|
|
{
|
|
CurrentTargetDelayLength = InDelayLength;
|
|
CurrentDelayLength.SetValue(NewDelayLength);
|
|
UpdatePhasorPhaseIncrement();
|
|
}
|
|
}
|
|
|
|
void FTapDelayPitchShifter::SetPitchShift(const float InPitchShift)
|
|
{
|
|
const float NewPitchShift = PitchShiftUtils::GetPitchShiftClamped(InPitchShift);
|
|
if (!FMath::IsNearlyEqual(NewPitchShift, CurrentPitchShift))
|
|
{
|
|
CurrentPitchShift = NewPitchShift;
|
|
CurrentPitchShiftRatio = Audio::GetFrequencyMultiplier(CurrentPitchShift);
|
|
UpdatePhasorPhaseIncrement();
|
|
}
|
|
}
|
|
|
|
void FTapDelayPitchShifter::SetPitchShiftRatio(const float InPitchShiftRatio)
|
|
{
|
|
if (!FMath::IsNearlyEqual(InPitchShiftRatio, CurrentPitchShiftRatio))
|
|
{
|
|
CurrentPitchShiftRatio = InPitchShiftRatio;
|
|
UpdatePhasorPhaseIncrement();
|
|
}
|
|
}
|
|
|
|
float FTapDelayPitchShifter::ReadDopplerShiftedTapFromDelay(const Audio::FDelay& InDelayBuffer, const float InReadOffsetMilliseconds)
|
|
{
|
|
// Update the interpolated delay length value
|
|
if (!CurrentDelayLength.IsDone())
|
|
{
|
|
UpdatePhasorPhaseIncrement();
|
|
CurrentDelayLength.GetNextValue();
|
|
}
|
|
|
|
// Compute the two tap delay read locations, one shifted 90 degrees out of phase
|
|
const float PhasorPhaseOffset = FMath::Fmod(PhasorPhase + 0.5f, 1.0f);
|
|
const float DelayTapRead1 = InReadOffsetMilliseconds + CurrentDelayLength.PeekCurrentValue() * PhasorPhase;
|
|
const float DelayTapRead2 = InReadOffsetMilliseconds + CurrentDelayLength.PeekCurrentValue() * PhasorPhaseOffset;
|
|
|
|
// This produces an overlapping cosine function that avoids pops in the output
|
|
const float DelayTapGain1 = FMath::Cos(PI * (PhasorPhase - 0.5f));
|
|
const float DelayTapGain2 = FMath::Cos(PI * (PhasorPhaseOffset - 0.5f));
|
|
|
|
// Update the phasor state
|
|
PhasorPhase += PhasorPhaseIncrement;
|
|
// Make sure we wrap to between 0.0 and 1.0 after incrementing the phase
|
|
PhasorPhase = FMath::Wrap(PhasorPhase, 0.0f, 1.0f);
|
|
|
|
// Read the delay lines at the given tap indices, apply the gains
|
|
const float Sample1 = DelayTapGain1 * InDelayBuffer.ReadDelayAt(DelayTapRead1);
|
|
const float Sample2 = DelayTapGain2 * InDelayBuffer.ReadDelayAt(DelayTapRead2);
|
|
|
|
return Sample1 + Sample2;
|
|
}
|
|
|
|
void FTapDelayPitchShifter::ProcessAudio(Audio::FDelay& InDelayBuffer, const float* InAudioBuffer, const int32 InNumFrames, float* OutAudioBuffer)
|
|
{
|
|
for (int32 FrameIndex = 0; FrameIndex < InNumFrames; ++FrameIndex)
|
|
{
|
|
OutAudioBuffer[FrameIndex] = ReadDopplerShiftedTapFromDelay(InDelayBuffer);
|
|
InDelayBuffer.WriteDelayAndInc(InAudioBuffer[FrameIndex]);
|
|
}
|
|
}
|
|
|
|
}
|