A modded EditSaber for making beat saber maps.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

662 lines
19 KiB

// Fill out your copyright notice in the Description page of Project Settings.
#include "eXiSoundVisPrivatePCH.h"
#include "SoundVisComponent.h"
#include "Sound/SoundWave.h"
#include "AudioDevice.h"
#include "Runtime/Engine/Public/VorbisAudioInfo.h"
#include "Developer/TargetPlatform/Public/Interfaces/IAudioFormat.h"
/// De-/Constructors
USoundVisComponent::USoundVisComponent()
{
AudioComponent = CreateDefaultSubobject<UAudioComponent>(FName("AudioComponent"));
PrimaryComponentTick.bCanEverTick = true;
}
USoundVisComponent::~USoundVisComponent()
{
}
void USoundVisComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction * ThisTickFunction)
{
UGameViewportClient* Viewport = GetWorld()->GetGameViewport();
if (bSoundPaused && bSoundPausedByBackgroundWindow)
{
if (Viewport->Viewport->IsForegroundWindow())
{
PrintLog(TEXT("Window is in foreground. Resuming!"));
BP_ResumeCalculatingFrequencySpectrum();
bSoundPausedByBackgroundWindow = false;
}
}
}
/// Functions to load Data from the HardDrive
bool USoundVisComponent::LoadSoundFileFromHD(const FString& InFilePath)
{
// Create new SoundWave Object
CompressedSoundWaveRef = NewObject<USoundWave>(USoundWave::StaticClass());
// Make sure the SoundWave Object is Valid
if (!CompressedSoundWaveRef) {
PrintError(TEXT("Failed to create new SoundWave Object!"));
return false;
}
// If true, the Sound was successfully loaded
bool bLoaded = false;
// TArray that holds the binary and encoded Sound data
TArray<uint8> RawFile;
// Load file into RawFile Array
bLoaded = FFileHelper::LoadFileToArray(RawFile, InFilePath.GetCharArray().GetData());
if (bLoaded)
{
UE_LOG(LogTemp, Error, TEXT("LoadSoundFileFromHD 0"));
// Fill the SoundData into the SoundWave Object
if (RawFile.Num() > 0) {
bLoaded = FillSoundWaveInfo(CompressedSoundWaveRef, &RawFile);
}
else {
PrintError(TEXT("RawFile Array is empty! Seams like Sound couldn't be loaded correctly."));
bLoaded = false;
}
// Get Pointer to the Compressed OGG Data
FByteBulkData* BulkData = &CompressedSoundWaveRef->CompressedFormatData.GetFormat(FName("OGG"));
// Set the Lock of the BulkData to ReadWrite
BulkData->Lock(LOCK_READ_WRITE);
// Copy compressed RawFile Data to the Address of the OGG Data of the SW File
FMemory::Memmove(BulkData->Realloc(RawFile.Num()), RawFile.GetData(), RawFile.Num());
// Unlock the BulkData again
BulkData->Unlock();
}
if (!bLoaded) {
PrintError(TEXT("Something went wrong while loading the Sound Data!"));
return false;
}
// Fill the PCMSampleBuffer
return GetPCMDataFromFile(CompressedSoundWaveRef);
}
bool USoundVisComponent::FillSoundWaveInfo(USoundWave* InSoundWave, TArray<uint8>* InRawFile)
{
// Info Structs
FSoundQualityInfo SoundQualityInfo;
FVorbisAudioInfo VorbisAudioInfo;
// Save the Info into SoundQualityInfo
if (!VorbisAudioInfo.ReadCompressedInfo(InRawFile->GetData(), InRawFile->Num(), &SoundQualityInfo))
{
return false;
}
// Fill in all the Data we have
InSoundWave->DecompressionType = EDecompressionType::DTYPE_RealTime;
InSoundWave->SoundGroup = ESoundGroup::SOUNDGROUP_Default;
InSoundWave->NumChannels = SoundQualityInfo.NumChannels;
InSoundWave->Duration = SoundQualityInfo.Duration;
InSoundWave->RawPCMDataSize = SoundQualityInfo.SampleDataSize;
InSoundWave->SampleRate = SoundQualityInfo.SampleRate;
return true;
}
/// Function to decompress the compressed Data that comes with the .ogg file
bool USoundVisComponent::GetPCMDataFromFile(USoundWave* InSoundWave)
{
if (InSoundWave == nullptr) {
PrintError(TEXT("Passed SoundWave pointer is a nullptr!"));
return false;
}
if (InSoundWave->NumChannels < 1 || InSoundWave->NumChannels > 2) {
PrintError(TEXT("SoundWave Object has not the right amount of Channels. Plugin only supports 1 or 2!"));
return false;
}
if (GEngine)
{
// Get a Pointer to the Main Audio Device
FAudioDevice* AudioDevice = GEngine->GetMainAudioDevice();
if (AudioDevice) {
InSoundWave->InitAudioResource(AudioDevice->GetRuntimeFormat(InSoundWave));
PrintLog(TEXT("Creating new DecompressWorker."));
// Creates a new DecompressWorker and starts it
InitNewDecompressTask(InSoundWave);
return true;
}
else {
PrintError(TEXT("Couldn't get a valid Pointer to the Main AudioDevice!"));
}
}
return false;
}
void USoundVisComponent::CalculateFrequencySpectrum(USoundWave* InSoundWaveRef, const float InStartTime, const float InDuration, TArray<float>& OutFrequencies)
{
// Clear the Array before continuing
OutFrequencies.Empty();
const int32 NumChannels = InSoundWaveRef->NumChannels;
const int32 SampleRate = InSoundWaveRef->SampleRate;
// Make sure the Number of Channels is correct
if (NumChannels > 0 && NumChannels <= 2)
{
// Check if we actually have a Buffer to work with
if (InSoundWaveRef->CachedRealtimeFirstBuffer)
{
// The first sample is just the StartTime * SampleRate
int32 FirstSample = SampleRate * InStartTime;
// The last sample is the SampleRate times (StartTime plus the Duration)
int32 LastSample = SampleRate * (InStartTime + InDuration);
// Get Maximum amount of samples in this Sound
const int32 SampleCount = InSoundWaveRef->RawPCMDataSize / (2 * NumChannels);
// An early check if we can create a Sample window
FirstSample = FMath::Min(SampleCount, FirstSample);
LastSample = FMath::Min(SampleCount, LastSample);
// Actual amount of samples we gonna read
int32 SamplesToRead = LastSample - FirstSample;
if (SamplesToRead < 0) {
PrintError(TEXT("Number of SamplesToRead is < 0!"));
return;
}
// Shift the window enough so that we get a PowerOfTwo. FFT works better with that
int32 PoT = 2;
while (SamplesToRead > PoT) {
PoT *= 2;
}
// Now we have a good PowerOfTwo to work with
SamplesToRead = PoT;
// Create two 2-dim Arrays for complex numbers | Buffer and Output
kiss_fft_cpx* Buffer[2] = { 0 };
kiss_fft_cpx* Output[2] = { 0 };
// Create 1-dim Array with one slot for SamplesToRead
int32 Dims[1] = { SamplesToRead };
kiss_fftnd_cfg STF = kiss_fftnd_alloc(Dims, 1, 0, nullptr, nullptr);
int16* SamplePtr = reinterpret_cast<int16*>(InSoundWaveRef->CachedRealtimeFirstBuffer);
// Allocate space in the Buffer and Output Arrays for all the data that FFT returns
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++)
{
Buffer[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);
Output[ChannelIndex] = (kiss_fft_cpx*)KISS_FFT_MALLOC(sizeof(kiss_fft_cpx) * SamplesToRead);
}
// Shift our SamplePointer to the Current "FirstSample"
SamplePtr += FirstSample * NumChannels;
for (int32 SampleIndex = 0; SampleIndex < SamplesToRead; SampleIndex++)
{
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++)
{
// Make sure the Point is Valid and we don't go out of bounds
if (SamplePtr != NULL && (SampleIndex + FirstSample < SampleCount))
{
// Use Window function to get a better result for the Data (Hann Window)
Buffer[ChannelIndex][SampleIndex].r = GetFFTInValue(*SamplePtr, SampleIndex, SamplesToRead);
Buffer[ChannelIndex][SampleIndex].i = 0.f;
}
else
{
// Use Window function to get a better result for the Data (Hann Window)
Buffer[ChannelIndex][SampleIndex].r = 0.f;
Buffer[ChannelIndex][SampleIndex].i = 0.f;
}
// Take the next Sample
SamplePtr++;
}
}
// Now that the Buffer is filled, use the FFT
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ChannelIndex++)
{
if (Buffer[ChannelIndex])
{
kiss_fftnd(STF, Buffer[ChannelIndex], Output[ChannelIndex]);
}
}
OutFrequencies.AddZeroed(SamplesToRead);
for (int32 SampleIndex = 0; SampleIndex < SamplesToRead; ++SampleIndex)
{
float ChannelSum = 0.0f;
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
if (Output[ChannelIndex])
{
// With this we get the actual Frequency value for the frequencies from 0hz to ~22000hz
ChannelSum += FMath::Sqrt(FMath::Square(Output[ChannelIndex][SampleIndex].r) + FMath::Square(Output[ChannelIndex][SampleIndex].i));
}
}
if (bNormalizeOutputToDb)
{
OutFrequencies[SampleIndex] = FMath::LogX(10, ChannelSum / NumChannels) * 10;
}
else
{
OutFrequencies[SampleIndex] = ChannelSum / NumChannels;
}
}
// Make sure to free up the FFT stuff
KISS_FFT_FREE(STF);
for (int32 ChannelIndex = 0; ChannelIndex < NumChannels; ++ChannelIndex)
{
KISS_FFT_FREE(Buffer[ChannelIndex]);
KISS_FFT_FREE(Output[ChannelIndex]);
}
}
else {
PrintError(TEXT("InSoundVisData.PCMData is a nullptr!"));
}
}
else {
PrintError(TEXT("Number of Channels is < 0!"));
}
}
float USoundVisComponent::GetFFTInValue(const int16 InSampleValue, const int16 InSampleIndex, const int16 InSampleCount)
{
float FFTValue = InSampleValue;
// Apply the Hann window
FFTValue *= 0.5f * (1 - FMath::Cos(2 * PI * InSampleIndex / (InSampleCount - 1)));
return FFTValue;
}
void USoundVisComponent::InitNewDecompressTask(USoundWave* InSoundWaveRef)
{
// Do we already have a valid Runnable? If not, create a new one
if (FAudioDecompressWorker::Runnable == NULL)
{
// Start Timer that watches the DecompressWorker State
GetWorld()->GetTimerManager().ClearTimer(AudioDecompressTimer);
GetWorld()->GetTimerManager().SetTimer(AudioDecompressTimer, this, &USoundVisComponent::Notify_SoundDecompressed, 0.1f, true);
// Init new Worker and pass the SoundWaveRef to decompress it
FAudioDecompressWorker::Runnable->InitializeWorker(InSoundWaveRef);
}
else if(FAudioDecompressWorker::Runnable->IsFinished())
{
// The Worker is finished and still valid, shut it down!
FAudioDecompressWorker::ShutdownWorker();
// Start Timer that watches the DecompressWorker State
GetWorld()->GetTimerManager().ClearTimer(AudioDecompressTimer);
GetWorld()->GetTimerManager().SetTimer(AudioDecompressTimer, this, &USoundVisComponent::Notify_SoundDecompressed, 0.1f, true);
// Init new Worker and pass the SoundWaveRef to decompress it
FAudioDecompressWorker::Runnable->InitializeWorker(InSoundWaveRef);
}
else {
PrintLog(TEXT("Worker not finished!"));
}
}
void USoundVisComponent::Notify_SoundDecompressed()
{
// If the Worker finished..
if (FAudioDecompressWorker::Runnable->IsFinished())
{
// ..clear the timer and..
GetWorld()->GetTimerManager().ClearTimer(AudioDecompressTimer);
try {
//..broadcast the result to the Blueprint
OnFileLoadCompleted.Broadcast(FAudioDecompressWorker::Runnable->GetSoundWaveRef());
} catch (const std::exception&) {
// Yo, bad things happened
}
PrintLog(TEXT("Worker finished!"));
}
else {
PrintLog(TEXT("Worker is working!"));
}
}
void USoundVisComponent::HandleFrequencySpectrumCalculation()
{
// Reference to the Client Viewport
UGameViewportClient* Viewport = GetWorld()->GetGameViewport();
// If the Window is not in the foreground, make sure to pause everything, so it won't get async!
if (!Viewport->Viewport->IsForegroundWindow() && bPauseWhenWindowInBackground)
{
PrintLog(TEXT("Window is not in foreground. Pausing!"));
BP_PauseCalculatingFrequencySpectrum();
bSoundPausedByBackgroundWindow = true;
}
// Only proceed if we are not over the duration, or stopped/pause calculating
if (CurrentSoundData && GetWorld()->GetTimerManager().GetTimerElapsed(SoundPlayerTimer) <= CurrentSoundData->Duration && bSoundPlaying)
{
// Dummy Array to Store the Frequencies in
TArray<float> OutFrequencies;
// Let our Function Calculate the Frequency Spectrum
CalculateFrequencySpectrum(CurrentSoundData, GetWorld()->GetTimerManager().GetTimerElapsed(SoundPlayerTimer), CurrentSegmentLength, OutFrequencies);
// Now that this is done, call the Delegate, so the Blueprint can work with it
OnFrequencySpectrumCalculated.Broadcast(OutFrequencies);
// Start this function again after a short delay
FrequencySpectrumTimerDelegate.BindUFunction(this, FName("HandleFrequencySpectrumCalculation"));
GetWorld()->GetTimerManager().SetTimer(FrequencySpectrumTimer, FrequencySpectrumTimerDelegate, 0.01f, false);
}
else
{
GetWorld()->GetTimerManager().ClearTimer(FrequencySpectrumTimer);
}
}
/// Blueprint Versions of the File Data Functions
bool USoundVisComponent::BP_LoadSoundFileFromHD(const FString InFilePath)
{
return LoadSoundFileFromHD(InFilePath);
}
void USoundVisComponent::BP_LoadAllSoundFileNamesFromHD(bool& bLoaded, const FString InDirectoryPath, const bool bInAbsolutePath, const FString InFileExtension, TArray<FString>& OutSoundFileNamesWithPath, TArray<FString>& OutSoundFileNamesWithoutPath)
{
FString FinalPath = InDirectoryPath;
if (!bInAbsolutePath)
{
FinalPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir()) + InDirectoryPath;
}
TArray<FString> DirectoriesToSkip;
IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
FLocalTimestampDirectoryVisitor Visitor(PlatformFile, DirectoriesToSkip, DirectoriesToSkip, false);
PlatformFile.IterateDirectory(*FinalPath, Visitor);
for (TMap<FString, FDateTime>::TIterator TimestampIt(Visitor.FileTimes); TimestampIt; ++TimestampIt)
{
const FString FilePath = TimestampIt.Key();
FString FileName = FPaths::GetCleanFilename(FilePath);
bool bShouldAddFile = true;
if (!InFileExtension.IsEmpty())
{
if (!(FPaths::GetExtension(FileName, false).Equals(InFileExtension, ESearchCase::IgnoreCase)))
{
bShouldAddFile = false;
}
}
if (bShouldAddFile)
{
OutSoundFileNamesWithPath.Add(FilePath);
FileName.FString::Split(FString("."), &FileName, nullptr, ESearchCase::IgnoreCase);
OutSoundFileNamesWithoutPath.Add(FileName);
}
}
bLoaded = true;
}
/// Blueprint Versions of the Analyze Functions
void USoundVisComponent::BP_CalculateFrequencySpectrum(USoundWave* InSoundWaveRef, const float InStartTime, const float InDuration, TArray<float>& OutFrequencies)
{
CalculateFrequencySpectrum(InSoundWaveRef, InStartTime, InDuration, OutFrequencies);
}
void USoundVisComponent::BP_StartCalculatingFrequencySpectrum(USoundWave* InSoundWaveRef, const float InSegmentLength)
{
// When the Sound Ref is NULL, better not start analyzing
if (InSoundWaveRef == nullptr) {
PrintError(TEXT("SoundWaveRef is a nullptr. Please load a Sound first!"));
return;
}
// Make sure we are not already playing something
if (!bSoundPlaying)
{
// If we pause, make sure to stop the Player first
if (GetWorld()->GetTimerManager().IsTimerPaused(SoundPlayerTimer))
{
BP_StopCalculatingFrequencySpectrum();
}
// Save the Current SoundVisData
CurrentSoundData = InSoundWaveRef;
// And the Current SegmentLength
CurrentSegmentLength = InSegmentLength;
// Set the Sound and Play it
AudioComponent->SetSound(InSoundWaveRef);
AudioComponent->Play();
// Mark Sound as playing
bSoundPlaying = true;
// Start Timer that is way too long, so we can use it as some kind of AudioPlayer Timer
GetWorld()->GetTimerManager().SetTimer(SoundPlayerTimer, 99999.f, false);
// Start the Calculation Loop
HandleFrequencySpectrumCalculation();
}
else {
PrintWarning(TEXT("AudioComponent is already Playing. Please stop it first!"));
}
}
void USoundVisComponent::BP_PauseCalculatingFrequencySpectrum()
{
// We can only pause, if we are playing
if (bSoundPlaying && !bSoundPaused)
{
// Stop the Audio Component
AudioComponent->Stop();
// Mark sound as being paused
bSoundPaused = true;
// Pause the timer
GetWorld()->GetTimerManager().PauseTimer(SoundPlayerTimer);
// Start the tick, so we can check when the Player is back in the game
SetComponentTickEnabled(true);
}
else {
PrintWarning(TEXT("You can't pause something, that is not playing!"));
}
}
void USoundVisComponent::BP_StopCalculatingFrequencySpectrum()
{
// If we are playing the Sound, or it's paused, stop it
if (bSoundPlaying || bSoundPaused)
{
// Stop the AudioComponent
AudioComponent->Stop();
// Mark Sound as not playing or paused
bSoundPaused = false;
bSoundPlaying = false;
bSoundPausedByBackgroundWindow = false;
// Clear the timer
GetWorld()->GetTimerManager().ClearTimer(SoundPlayerTimer);
// Clear Current SoundVisData
CurrentSoundData = nullptr;
// Clear CurrentSegmentLength
CurrentSegmentLength = 0.0f;
// Stop the Tick Function
SetComponentTickEnabled(false);
}
else {
PrintWarning(TEXT("You can't stop something, that is not playing or paused!"));
}
}
void USoundVisComponent::BP_ResumeCalculatingFrequencySpectrum()
{
if (bSoundPlaying && bSoundPaused)
{
// Start the Sound at the Point where we left it
AudioComponent->Play(GetWorld()->GetTimerManager().GetTimerElapsed(SoundPlayerTimer));
// Mark sound as no longer paused
bSoundPaused = false;
// UnPause the Timer again
GetWorld()->GetTimerManager().UnPauseTimer(SoundPlayerTimer);
// And start the Calculation again
HandleFrequencySpectrumCalculation();
// Stop the Tick Function
SetComponentTickEnabled(false);
}
else {
PrintWarning(TEXT("AudioComponent is Playing or not paused!"));
}
}
bool USoundVisComponent::IsPlayerPlaying()
{
return (bSoundPlaying && !bSoundPaused);
}
bool USoundVisComponent::IsPlayerPaused()
{
UWorld* World = GetWorld();
if (World)
{
return (bSoundPlaying && bSoundPaused);
}
return false;
}
float USoundVisComponent::GetCurrentPlayBackTime()
{
UWorld* World = GetWorld();
if (World)
{
return World->GetTimerManager().GetTimerElapsed(SoundPlayerTimer);
}
return 0.0f;
}
/// Frequency Data Functions
void USoundVisComponent::BP_GetSpecificFrequencyValue(USoundWave* InSoundWave, TArray<float> InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue)
{
if (InSoundWave && InFrequencies.Num() > 0 && (int32)(InWantedFrequency * InFrequencies.Num() * 2 / InSoundWave->SampleRate) < InFrequencies.Num())
{
OutFrequencyValue = InFrequencies[(int32)(InWantedFrequency * InFrequencies.Num() * 2 / InSoundWave->SampleRate)];
}
}
void USoundVisComponent::BP_GetAverageSubBassValue(USoundWave* InSoundWave, TArray<float> InFrequencies, float& OutAverageSubBass)
{
BP_GetAverageFrequencyValueInRange(InSoundWave, InFrequencies, 20, 60, OutAverageSubBass);
}
void USoundVisComponent::BP_GetAverageBassValue(USoundWave* InSoundWave, TArray<float> InFrequencies, float& OutAverageBass)
{
if (InSoundWave)
{
BP_GetAverageFrequencyValueInRange(InSoundWave, InFrequencies, 60, 250, OutAverageBass);
}
}
void USoundVisComponent::BP_GetAverageFrequencyValueInRange(USoundWave* InSoundWave, TArray<float> InFrequencies, int32 InStartFrequence, int32 InEndFrequence, float& OutAverageFrequency)
{
// Init the Return Value
OutAverageFrequency = 0.0f;
if (InSoundWave == nullptr)
return;
if (InStartFrequence >= InEndFrequence || InStartFrequence < 0 || InEndFrequence > 22000)
return;
int32 FStart = (int32)(InStartFrequence * InFrequencies.Num() * 2 / InSoundWave->SampleRate);
int32 FEnd = (int32)(InEndFrequence * InFrequencies.Num() * 2 / InSoundWave->SampleRate);
if (FStart < 0 || FEnd >= InFrequencies.Num())
return;
int32 NumberOfFrequencies = 0;
float ValueSum = 0.0f;
for (int i = FStart; i <= FEnd; i++)
{
NumberOfFrequencies++;
ValueSum += InFrequencies[i];
}
OutAverageFrequency = ValueSum / NumberOfFrequencies;
}