알라딘MGG와이드바


[UE4] SoundVisualization 개발 이야기

https://www.youtube.com/watch?v=ix3oa7nB2VA 를 UE 4.26 C++ 로 포팅.
역시 BP 보다는 C++ 이 깔끔하지, 이러고 있었는데 빌드해 보니 linking 에러 발생.
SoundVisualizationStatics.h 파일 열어보니까 

class USoundVisualizationStatics : public UBlueprintFunctionLibrary
{
static void CalculateFrequencySpectrum(USoundWave* SoundWave, const bool bSplitChannels, const float StartTime, const float TimeLength, const int32 SpectrumWidth, TArray< TArray<float> >& OutSpectrums);

UFUNCTION(BlueprintCallable, Category="SoundVisualization")
static void CalculateFrequencySpectrum(USoundWave* SoundWave, int32 Channel, float StartTime, float TimeLength, int32 SpectrumWidth, TArray<float>& OutSpectrum);

와 같이 되어 있음.
class SOUNDVISUALIZATIONS_API  USoundVisualizationStatics : public UBlueprintFunctionLibrary
이렇게 되어 있어야 하지 않았을까?

심지어 USoundVisualizationStatics::CalculateFrequencySpectrum 구현부를 보면
#if WITH_EDITORONLY_DATA 으로 감싸져 있음. -.-;;;

결국 plug-in 코드를 C++ 프로젝트에 그대로 복붙한다.
.Build.cs 파일을 다음과 같이 수정.

using UnrealBuildTool;

public class SoundVis : ModuleRules
{
public SoundVis(ReadOnlyTargetRules Target) : base(Target)
{
// PrivateDependencyModuleNames.AddRange(new string[] {"SoundVisualizations"});  // 링킹 에러가 나니 이건 의미가 없다.

if (Target.Platform == UnrealTargetPlatform.Win64 || Target.Platform == UnrealTargetPlatform.Win32)
{
  if (Target.WindowsPlatform.bNeedsLegacyStdioDefinitionsLib)
  {
PublicSystemLibraries.Add("legacy_stdio_definitions.lib");
  }
}

AddEngineThirdPartyPrivateStaticDependencies(Target, "Kiss_FFT");
}
}

플로그인 코드에 있는 함수를 복붙

float GetFFTInValue(const int16 SampleValue, const int16 SampleIndex, const int16 SampleCount)
{
float FFTValue = SampleValue;

// Apply the Hann window
FFTValue *= 0.5f * (1 - FMath::Cos(2 * PI * SampleIndex / (SampleCount - 1)));

return FFTValue;
}

static void CalculateFrequencySpectrum(USoundWave* SoundWave, const bool bSplitChannels, const float StartTime, const float TimeLength, const int32 SpectrumWidth, TArray< TArray<float> >& OutSpectrums)
{
OutSpectrums.Empty();

const int32 NumChannels = SoundWave->NumChannels;
if (SpectrumWidth > 0 && NumChannels > 0)
{
// Setup the output data
OutSpectrums.AddZeroed((bSplitChannels ? NumChannels : 1));
for (int32 ChannelIndex = 0; ChannelIndex < OutSpectrums.Num(); ++ChannelIndex)
{
OutSpectrums[ChannelIndex].AddZeroed(SpectrumWidth);
}

// check if there is any raw sound data
if (SoundWave->RawData.GetBulkDataSize() > 0)
{
// Lock raw wave data.
uint8* RawWaveData = (uint8*)SoundWave->RawData.Lock(LOCK_READ_ONLY);
int32 RawDataSize = SoundWave->RawData.GetBulkDataSize();
FWaveModInfo WaveInfo;

// parse the wave data
if (WaveInfo.ReadWaveHeader(RawWaveData, RawDataSize, 0))
{
int32 SampleCount = 0;
int32 SampleCounts[10] = { 0 };

int32 FirstSample = *WaveInfo.pSamplesPerSec * StartTime;
int32 LastSample = *WaveInfo.pSamplesPerSec * (StartTime + TimeLength);

if (NumChannels <= 2)
{
SampleCount = WaveInfo.SampleDataSize / (2 * NumChannels);
}
else
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
SampleCounts[ChannelIndex] = (SoundWave->ChannelSizes[ChannelIndex] / 2);
SampleCount = FMath::Max(SampleCount, SampleCounts[ChannelIndex]);
SampleCounts[ChannelIndex] -= FirstSample;
}
}

FirstSample = FMath::Min(SampleCount, FirstSample);
LastSample = FMath::Min(SampleCount, LastSample);

int32 SamplesToRead = LastSample - FirstSample;

if (SamplesToRead > 0)
{
// Shift the window enough so that we get a power of 2
int32 PoT = 2;
while (SamplesToRead > PoT) PoT *= 2;
FirstSample = FMath::Max(0, FirstSample - (PoT - SamplesToRead) / 2);
SamplesToRead = PoT;
LastSample = FirstSample + SamplesToRead;
if (LastSample > SampleCount)
{
FirstSample = LastSample - SamplesToRead;
}
if (FirstSample < 0)
{
// If we get to this point we can't create a reasonable window so just give up
SoundWave->RawData.Unlock();
return;
}

kiss_fft_cpx* buf[10] = { 0 };
kiss_fft_cpx* out[10] = { 0 };

int32 Dims[1] = { SamplesToRead };
kiss_fftnd_cfg stf = kiss_fftnd_alloc(Dims, 1, 0, NULL, NULL);


const int16* SamplePtr = reinterpret_cast<const int16*>(WaveInfo.SampleDataStart);
if (NumChannels <= 2)
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
buf[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);
out[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);
}

SamplePtr += (FirstSample * NumChannels);

for (int32 SampleIndex = 0; SampleIndex < SamplesToRead; ++SampleIndex)
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
buf[ChannelIndex][SampleIndex].r = GetFFTInValue(*SamplePtr, SampleIndex, SamplesToRead);
buf[ChannelIndex][SampleIndex].i = 0.f;

SamplePtr++;
}
}
}
else
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
// Drop this channel out if there isn't the power of 2 number of samples available
if (SampleCounts[ChannelIndex] >= SamplesToRead)
{
buf[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);
out[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);

for (int32 SampleIndex = 0; SampleIndex < SamplesToRead; ++SampleIndex)
{
buf[ChannelIndex][SampleIndex].r = GetFFTInValue(*(SamplePtr + FirstSample + SampleIndex + SoundWave->ChannelOffsets[ChannelIndex] / 2), SampleIndex, SamplesToRead);
buf[ChannelIndex][SampleIndex].i = 0.f;
}
}
}
}

for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
if (buf[ChannelIndex])
{
kiss_fftnd(stf, buf[ChannelIndex], out[ChannelIndex]);
}
}

int32 SamplesPerSpectrum = SamplesToRead / (2 * SpectrumWidth);
int32 ExcessSamples = SamplesToRead % (2 * SpectrumWidth);

int32 FirstSampleForSpectrum = 1;
for (int32 SpectrumIndex = 0; SpectrumIndex < SpectrumWidth; ++SpectrumIndex)
{
static bool doLog = false;

int32 SamplesRead = 0;
double SampleSum = 0;
int32 SamplesForSpectrum = SamplesPerSpectrum + (ExcessSamples-- > 0 ? 1 : 0);
ensure(SamplesForSpectrum > 0);

if (doLog) UE_LOG(LogTemp, Log, TEXT("----"));
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
if (out[ChannelIndex])
{
if (bSplitChannels)
{
SampleSum = 0;
}

for (int32 SampleIndex = 0; SampleIndex < SamplesForSpectrum; ++SampleIndex)
{
float PostScaledR = out[ChannelIndex][FirstSampleForSpectrum + SampleIndex].r * 2.f / SamplesToRead;
float PostScaledI = out[ChannelIndex][FirstSampleForSpectrum + SampleIndex].i * 2.f / SamplesToRead;
//float Val = FMath::Sqrt(FMath::Square(PostScaledR) + FMath::Square(PostScaledI));
//float Val = 10.f * FMath::LogX(10.f, FMath::Square(PostScaledR) + FMath::Square(PostScaledI));  // 원본 코드
const float LogValue = FMath::Square(PostScaledR) + FMath::Square(PostScaledI);
if (LogValue != 0)
{
float Val = 10.f * FMath::LogX(10.f, LogValue);  // LogValue 가 0 이면 LogX 가 -inf 를 리턴하면서 crash 발생
if (doLog) UE_LOG(LogTemp, Log, TEXT("%.2f"), Val);
SampleSum += Val;
}
}

if (bSplitChannels)
{
OutSpectrums[ChannelIndex][SpectrumIndex] = (float)(SampleSum / SamplesForSpectrum);
}
SamplesRead += SamplesForSpectrum;
}
}

if (!bSplitChannels)
{
ensure(SamplesRead > 0);
OutSpectrums[0][SpectrumIndex] = (float)(SampleSum / SamplesRead);
}

FirstSampleForSpectrum += SamplesForSpectrum;
}

KISS_FFT_FREE(stf);
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
if (buf[ChannelIndex])
{
KISS_FFT_FREE(buf[ChannelIndex]);
KISS_FFT_FREE(out[ChannelIndex]);
}
}
}
}

SoundWave->RawData.Unlock();
}
}
}

나머지는 BP 버전과 유사하게 구현

///////////////////////////////////////////////////////////////
// header 파일
#include "EngineMinimal.h"
#include "GameFramework/Actor.h"
#include "Components/AudioComponent.h"
#include "EQController.generated.h"

UCLASS()
class SOUNDVIS_API AEQController : public AActor
{
GENERATED_BODY()

public:
UPROPERTY(EditAnywhere, BlueprintReadWrite)
UAudioComponent* Audio;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
USoundWave* SoundWave;

UPROPERTY(EditAnywhere)
TSubclassOf<class AActor> CubeBlueprint;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 NumberOfCubes = 20;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
float Distance = 50.f;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
float ActorMoveZScale = 5.f;

// Sets default values for this actor's properties
AEQController();

protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
void SpawnGrid();

public:
// Called every frame
virtual void Tick(float DeltaTime) override;

private:
TArray<AActor*> CubeArray;
};

///////////////////////////////////////////////////////////////
// cpp 파일
#include "EQController.h"
// #include "SoundVisualizationStatics.h"  // 의미없다.
#include "Audio.h"  // #include "SoundVisualizationStatics.h" 에서 가져왔다.
#include "Sound/SoundWave.h"  // #include "SoundVisualizationStatics.h"  에서 가져왔다.
#include "tools/kiss_fftnd.h"  // #include "SoundVisualizationStatics.h"  에서 가져왔다.

AEQController::AEQController()
{
PrimaryActorTick.bCanEverTick = true;
Audio = CreateDefaultSubobject<UAudioComponent>(TEXT("Audio"));
const float Val = FMath::LogX(10.f, FMath::Square(0.f) + FMath::Square(0.f));  // -inf 리턴한다.
UE_LOG(LogTemp, Log, TEXT("Val [%0.2f][%0.2f][%0.2f]"), Val);
}

void AEQController::BeginPlay()
{
Super::BeginPlay();
Audio->Play();
SpawnGrid();
}

void AEQController::SpawnGrid()
{
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
FRotator Rotator;

UWorld* World = GetWorld();
FVector SpawnLocation = FVector::ZeroVector;
for (int32 Row = 0; Row < NumberOfCubes; ++Row)
{
SpawnLocation.X = Row * Distance;
CubeArray.Add(World->SpawnActor<AActor>(CubeBlueprint, SpawnLocation, Rotator, SpawnParams));
}
}

void AEQController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

const float InGameTimeInSeconds = GetGameTimeSinceCreation();
TArray< TArray<float> > Spectrums;
CalculateFrequencySpectrum(SoundWave, false, InGameTimeInSeconds, DeltaTime, CubeArray.Num(), Spectrums);

TArray<float>& SpectrumsValue = Spectrums[0];
for (int32 Idx = 0; Idx < CubeArray.Num(); ++Idx)
{
AActor* ItActor = CubeArray[Idx];
FVector ActorPos = ItActor->GetActorLocation();
ensure(SpectrumsValue[Idx] < 1000.f && SpectrumsValue[Idx] > -1000.f);
ActorPos.Z = (SpectrumsValue[Idx] * ActorMoveZScale);
ItActor->SetActorLocation(ActorPos);
}
}

결과는 다음과 같다. 음악에 따라 움직이기는 하는데, WinAmp 에서 보던 그런 느낌은 안 나는데... 뭘 잘 못 했나 싶기도 하고...

NuRi's Tools - iframe 변환기


덧글

  • 발라 2021/11/07 00:25 # 답글

    윈앰프라고 하면 최고점이 낙하하는 그 맛이 있어야...
  • 박PD 2021/11/07 17:44 #

    맞아요. 지금 봐도 그 당시 그 EQ 연출은 멋지다고 생각합니다.
댓글 입력 영역


Yes24위대한게임의탄생3

위대한 게임의 탄생 3
예스24 | 애드온2