Files
UnrealEngine/Engine/Source/Runtime/SignalProcessing/Private/TapDelayPitchShifter.cpp
2025-05-18 13:04:45 +08:00

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]);
}
}
}