mirror of
https://github.com/Theaninova/BeatLanguageMapper.git
synced 2026-01-21 01:13:04 +00:00
Initial Commit!
いきますよ!
This commit is contained in:
17
Plugins/eXiSoundVis/.gitattributes
vendored
Normal file
17
Plugins/eXiSoundVis/.gitattributes
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
49
Plugins/eXiSoundVis/.gitignore
vendored
Normal file
49
Plugins/eXiSoundVis/.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# =========================
|
||||
# Operating System Files
|
||||
# =========================
|
||||
|
||||
# OSX
|
||||
# =========================
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
/Binaries
|
||||
/Intermediate
|
||||
123
Plugins/eXiSoundVis/Readme.md
Normal file
123
Plugins/eXiSoundVis/Readme.md
Normal file
@@ -0,0 +1,123 @@
|
||||
[](https://www.youtube.com/watch?v=N4eA68BEpak)
|
||||
<sup>**Note:** The image above is an animated GIF, so there's some quality lost. Please see the [YouTube video](https://www.youtube.com/watch?v=N4eA68BEpak) for higher quality visualization of Disclosure's You and Me.</sup>
|
||||
|
||||
This is an Unreal Engine 4 plugin that loads `.ogg` files at runtime and analyzes them to get the frequency spectrum to control gameplay, visualization and more.
|
||||
|
||||
|
||||
Feature list and pictures of available nodes:
|
||||
---------------------------------------------
|
||||
|
||||
* Load `.ogg` file from HDD
|
||||
* Load sound names and file paths from HDD to display list of sound
|
||||
* Get frequency spectrum of a loaded Sound
|
||||
* Get specific frequency values by using the calculated frequency spectrum
|
||||
* Start/Pause/Resume/Stop your Sound and Frequency Calculation through the build in Player
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Installation
|
||||
-------------
|
||||
Unzip the package into the Plugins directory of your game.
|
||||
To add it as an engine plugin you will need to unzip the module into the plugin directory under where you installed UE4.
|
||||
|
||||
|
||||
**1.** Download the ZIP File.
|
||||
|
||||
**2.** Create a `Plugins` folder in your game or engine directory and extract the plugin into it. It should look something like this:
|
||||
|
||||

|
||||
|
||||
**3.** Open your project (and/or regenerate the Visual Studio files to have the plugin in your solution) and enable it in the plugin section:
|
||||
|
||||

|
||||
|
||||
**4.** To use the Plugin, add the `SoundVisComponent` to the Actor of your choice, which comes with the plugin:
|
||||
|
||||

|
||||
|
||||
**5.** Load a sound via its **ABSOLUTE** path (only `.ogg` files). The Component has a `Delegate | OnFileLoadCompleted`, which gets called and passes the complete `USoundWave` Reference, once the process is over (ASYNC)!:
|
||||
|
||||

|
||||
|
||||
**5.1** NEVER CLOSE THE PROJECT WHILE LOADING A SOUND!
|
||||
|
||||
**6.** Use the Calculate Frequency Spectrum function after you loaded a sound to get an `Array of Frequency Values`, which represents the Frequencies from 0 to ~22000hz:
|
||||
|
||||

|
||||
|
||||
**6.1** THIS ONLY WORKS WITH LOADED AND DECOMPRESSED .ogg FILES!
|
||||
|
||||
**7.** Since this only analyzes one small segment of the Sound and we don't want ugly `Delay-Loops`, use these functions to Start/Pause/Resume/Stop the whole sound (will also play it!)
|
||||
|
||||

|
||||
|
||||
**7.1** The analyzed Frequency Spectrum will be returned via a second `Delegate | OnFrequencySpectrumCalculated`, which also comes with the Component.
|
||||
|
||||

|
||||
|
||||
**8.** Now you can use the different frequency functions to get the values (for example if you want to get the values for bass, use 20 to 60 for SubBass and 60 to 250 for Bass. You can look up more on the internet.
|
||||
|
||||

|
||||
|
||||
**11.** You can find all functions available by going to the SoundVis category. They are explained in the [`SoundVisComponent.h`](https://github.com/eXifreXi/eXiSoundVis/blob/4.11/Source/eXiSoundVis/Public/SoundVisComponent.h) if you don't know how to use them:
|
||||
|
||||

|
||||
|
||||
When cooking, make sure to add the plugin to your projects dependencies!
|
||||
|
||||

|
||||
|
||||
|
||||
Example project
|
||||
---------------
|
||||
|
||||
|
||||
- 4.16 (No idea if properly commented or not and rather ugly, but maybe it helps!): http://www.cedric-neukirchen.net/Downloads/SoundVisDemo.7z
|
||||
|
||||
License
|
||||
-------------
|
||||
By using this plugin you accept the CC-BY 4.0 license and
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Cedric Neukirchen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
Contact
|
||||
-------------
|
||||
If you have any Questions, Comments, Bug reports or feature requests for this plugin,
|
||||
or you wish to contact me you can:
|
||||
|
||||
email me - cedric.neukirchen@gmx.de
|
||||
|
||||
contact me on the forum - Username: eXi
|
||||
|
||||
contact me on the Discord Unreal-Slackers Server - Username: cedric_exi
|
||||
|
||||
|
||||
Credits
|
||||
--------------
|
||||
n00854180t - Helping a lot to get the loading of the .ogg file running
|
||||
|
||||
moss - Helping to understand wave files
|
||||
BIN
Plugins/eXiSoundVis/Resources/Icon128.png
Normal file
BIN
Plugins/eXiSoundVis/Resources/Icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,128 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#include "eXiSoundVisPrivatePCH.h"
|
||||
|
||||
#include "AudioDecompressWorker.h"
|
||||
#include "SoundVisComponent.h"
|
||||
|
||||
#include "AudioDevice.h"
|
||||
#include "Developer/TargetPlatform/Public/Interfaces/IAudioFormat.h"
|
||||
|
||||
FAudioDecompressWorker* FAudioDecompressWorker::Runnable = NULL;
|
||||
int32 FAudioDecompressWorker::ThreadCounter = 0;
|
||||
|
||||
FAudioDecompressWorker::FAudioDecompressWorker(class USoundWave* InSoundWaveRef)
|
||||
: SoundWaveRef(InSoundWaveRef)
|
||||
, AudioInfo(NULL)
|
||||
, Thread(NULL)
|
||||
{
|
||||
if (GEngine && GEngine->GetMainAudioDevice())
|
||||
{
|
||||
AudioInfo = GEngine->GetMainAudioDevice()->CreateCompressedAudioInfo(SoundWaveRef);
|
||||
}
|
||||
|
||||
// Higher overall ThreadCounter to avoid duplicated names
|
||||
FAudioDecompressWorker::ThreadCounter++;
|
||||
|
||||
Thread = FRunnableThread::Create(this, *FString::Printf(TEXT("FAudioDecompressWorker%d"), FAudioDecompressWorker::ThreadCounter), 0, EThreadPriority::TPri_Normal);
|
||||
}
|
||||
|
||||
FAudioDecompressWorker::~FAudioDecompressWorker()
|
||||
{
|
||||
delete Thread;
|
||||
Thread = NULL;
|
||||
}
|
||||
|
||||
FAudioDecompressWorker* FAudioDecompressWorker::InitializeWorker(class USoundWave* InSoundWaveRef)
|
||||
{
|
||||
Runnable = new FAudioDecompressWorker(InSoundWaveRef);
|
||||
|
||||
return Runnable;
|
||||
}
|
||||
|
||||
void FAudioDecompressWorker::ShutdownWorker()
|
||||
{
|
||||
if (Runnable)
|
||||
{
|
||||
Runnable->EnsureCompletion();
|
||||
delete Runnable;
|
||||
Runnable = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
bool FAudioDecompressWorker::Init()
|
||||
{
|
||||
// Make sure the Worker is marked is not finished
|
||||
bIsFinished = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 FAudioDecompressWorker::Run()
|
||||
{
|
||||
if (!SoundWaveRef)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (AudioInfo != NULL)
|
||||
{
|
||||
FSoundQualityInfo QualityInfo = { 0 };
|
||||
|
||||
// Parse the audio header for the relevant information
|
||||
if (!(SoundWaveRef->ResourceData))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (AudioInfo->ReadCompressedInfo(SoundWaveRef->ResourceData, SoundWaveRef->ResourceSize, &QualityInfo))
|
||||
{
|
||||
FScopeCycleCounterUObject WaveObject(SoundWaveRef);
|
||||
|
||||
// Extract the data
|
||||
SoundWaveRef->SampleRate = QualityInfo.SampleRate;
|
||||
SoundWaveRef->NumChannels = QualityInfo.NumChannels;
|
||||
|
||||
if (QualityInfo.Duration > 0.0f)
|
||||
{
|
||||
SoundWaveRef->Duration = QualityInfo.Duration;
|
||||
}
|
||||
|
||||
const uint32 PCMBufferSize = SoundWaveRef->Duration * SoundWaveRef->SampleRate * SoundWaveRef->NumChannels;
|
||||
|
||||
SoundWaveRef->CachedRealtimeFirstBuffer = new uint8[PCMBufferSize * 2];
|
||||
|
||||
AudioInfo->SeekToTime(0.0f);
|
||||
AudioInfo->ReadCompressedData(SoundWaveRef->CachedRealtimeFirstBuffer, false, PCMBufferSize * 2);
|
||||
}
|
||||
else if (SoundWaveRef->DecompressionType == DTYPE_RealTime || SoundWaveRef->DecompressionType == DTYPE_Native)
|
||||
{
|
||||
SoundWaveRef->RemoveAudioResource();
|
||||
}
|
||||
|
||||
delete AudioInfo;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FAudioDecompressWorker::Stop()
|
||||
{
|
||||
StopTaskCounter.Increment();
|
||||
}
|
||||
|
||||
void FAudioDecompressWorker::Exit()
|
||||
{
|
||||
// Make sure to mark Thread as finished
|
||||
bIsFinished = true;
|
||||
}
|
||||
|
||||
void FAudioDecompressWorker::EnsureCompletion()
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (Thread != NULL) {
|
||||
|
||||
Thread->WaitForCompletion();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
// 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
|
||||
|
||||
void 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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Fill the PCMSampleBuffer
|
||||
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
|
||||
|
||||
void USoundVisComponent::GetPCMDataFromFile(USoundWave* InSoundWave)
|
||||
{
|
||||
if (InSoundWave == nullptr) {
|
||||
|
||||
PrintError(TEXT("Passed SoundWave pointer is a nullptr!"));
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
else {
|
||||
|
||||
PrintError(TEXT("Couldn't get a valid Pointer to the Main AudioDevice!"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
//..broadcast the result to the Blueprint
|
||||
OnFileLoadCompleted.Broadcast(FAudioDecompressWorker::Runnable->GetSoundWaveRef());
|
||||
|
||||
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
|
||||
|
||||
void USoundVisComponent::BP_LoadSoundFileFromHD(const FString InFilePath)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "eXiSoundVisPrivatePCH.h"
|
||||
|
||||
// Log Category
|
||||
DEFINE_LOG_CATEGORY(LogeXiSoundVis);
|
||||
|
||||
void FeXiSoundVisModule::StartupModule()
|
||||
{
|
||||
// This code will execute after your module is loaded into memory (but after global variables are initialized, of course.)
|
||||
}
|
||||
|
||||
void FeXiSoundVisModule::ShutdownModule()
|
||||
{
|
||||
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
|
||||
// we call this function before unloading the module.
|
||||
}
|
||||
|
||||
IMPLEMENT_MODULE(FeXiSoundVisModule, eXiSoundVis)
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "Engine.h"
|
||||
#include "eXiSoundVis.h"
|
||||
|
||||
// KISS Headers, that we need for the decompression part
|
||||
#include "ThirdParty/Kiss_FFT/kiss_fft129/kiss_fft.h"
|
||||
#include "ThirdParty/Kiss_FFT/kiss_fft129/tools/kiss_fftnd.h"
|
||||
|
||||
// Log Category
|
||||
DECLARE_LOG_CATEGORY_EXTERN(LogeXiSoundVis, Log, All);
|
||||
|
||||
// Short Defines to faster debug
|
||||
#define PrintLog(TextToLog) if(bShowLogDebug) UE_LOG(LogeXiSoundVis, Log, TextToLog)
|
||||
#define PrintWarning(TextToLog) if(bShowWarningDebug) UE_LOG(LogeXiSoundVis, Warning, TextToLog)
|
||||
#define PrintError(TextToLog) if(bShowErrorDebug) UE_LOG(LogeXiSoundVis, Error, TextToLog)
|
||||
@@ -0,0 +1,73 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AudioDecompress.h"
|
||||
|
||||
//#include "AudioDecompressWorker.generated.h"
|
||||
|
||||
class FAudioDecompressWorker : public FRunnable
|
||||
{
|
||||
/// VARIABLES ///
|
||||
|
||||
public:
|
||||
|
||||
// Singleton Instance, can access the Thread any time via static accessors
|
||||
static FAudioDecompressWorker* Runnable;
|
||||
|
||||
// Thread to run the worker FRunnable on
|
||||
FRunnableThread* Thread;
|
||||
|
||||
private:
|
||||
|
||||
// Stop this thread? Uses Thread Safe Counter
|
||||
FThreadSafeCounter StopTaskCounter;
|
||||
|
||||
// Bool to check if the thread is running
|
||||
bool bIsFinished;
|
||||
|
||||
// Counter for the ThreadNames
|
||||
static int32 ThreadCounter;
|
||||
|
||||
protected:
|
||||
|
||||
// Pointer to SoundWave Object we want to Decompress
|
||||
class USoundWave* SoundWaveRef;
|
||||
|
||||
// Some Compressed Audio Information
|
||||
ICompressedAudioInfo* AudioInfo;
|
||||
|
||||
/// FUNCTIONS ///
|
||||
|
||||
public:
|
||||
|
||||
// Constructor for my De-Compressor
|
||||
FAudioDecompressWorker(class USoundWave* InSoundWaveRef);
|
||||
~FAudioDecompressWorker();
|
||||
|
||||
// Custom Init function
|
||||
static FAudioDecompressWorker* InitializeWorker(class USoundWave* InSoundWaveRef);
|
||||
|
||||
// Custom Shutdown function
|
||||
static void ShutdownWorker();
|
||||
|
||||
// Start FRunnable Interface
|
||||
virtual bool Init();
|
||||
virtual uint32 Run();
|
||||
virtual void Stop();
|
||||
virtual void Exit();
|
||||
// End FRunnable Interface
|
||||
|
||||
// Make sure Thread completed
|
||||
void EnsureCompletion();
|
||||
|
||||
/// GETTER / SETTER
|
||||
|
||||
class USoundWave* GetSoundWaveRef() const {
|
||||
return SoundWaveRef;
|
||||
}
|
||||
|
||||
bool IsFinished() const {
|
||||
return bIsFinished;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,305 @@
|
||||
// Fill out your copyright notice in the Description page of Project Settings.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Components/ActorComponent.h"
|
||||
|
||||
#include "AudioDecompress.h"
|
||||
|
||||
// Workers, to have an Async Decompress worker
|
||||
#include "Runtime/Core/Public/Async/AsyncWork.h"
|
||||
|
||||
#include "AudioDecompressWorker.h"
|
||||
|
||||
#include "SoundVisComponent.generated.h"
|
||||
|
||||
USTRUCT(BlueprintType, Blueprintable)
|
||||
struct FSoundVisData
|
||||
{
|
||||
GENERATED_USTRUCT_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "eXiSoundVis | Sounds")
|
||||
class USoundWave* SoundWaveRef;
|
||||
|
||||
TSharedPtr<uint8> PCMData;
|
||||
|
||||
~FSoundVisData()
|
||||
{
|
||||
SoundWaveRef = nullptr;
|
||||
PCMData.Reset();
|
||||
}
|
||||
};
|
||||
|
||||
// Delegate that passes FSoundVisData
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFileLoadCompleted, USoundWave*, SoundWaveRef);
|
||||
|
||||
// Delegate used by the Worker
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FWorkerFinished, USoundWave*, SoundWaveRef);
|
||||
|
||||
// Delegate that passes FrequencyValues
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FFrequencySpectrumCalculated, const TArray<float>&, OutFrequencies);
|
||||
|
||||
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent), meta=(DisplayName = "SoundVisComponent") )
|
||||
class EXISOUNDVIS_API USoundVisComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/// VARIABLES ///
|
||||
|
||||
private:
|
||||
|
||||
// Reference to the SoundWave we are decompressing
|
||||
USoundWave* CompressedSoundWaveRef;
|
||||
|
||||
/// Blueprint Exposed Variables
|
||||
|
||||
public:
|
||||
|
||||
// Audio Component to Play our Audio with
|
||||
UPROPERTY(BlueprintReadOnly, Category = "SoundVis | Sound Player")
|
||||
class UAudioComponent* AudioComponent;
|
||||
|
||||
// TimerHandle for the SoundPlayer, that only exists to check how long a Sound is running
|
||||
UPROPERTY(BlueprintReadOnly, Category = "SoundVis | Frequency Spectrum")
|
||||
FTimerHandle SoundPlayerTimer;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "SoundVis | Sound Player")
|
||||
bool bSoundPlaying;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "SoundVis | Sound Player")
|
||||
bool bSoundPaused;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "SoundVis | Sound Player")
|
||||
bool bSoundPausedByBackgroundWindow = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoundVis | Sound Player")
|
||||
bool bPauseWhenWindowInBackground = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "SoundVis | Sound Player")
|
||||
bool bNormalizeOutputToDb = false;
|
||||
|
||||
/// Delegates
|
||||
|
||||
// Blueprint Delegate that gets Broadcasted when the File is loaded completely
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FFileLoadCompleted OnFileLoadCompleted;
|
||||
|
||||
// Blueprint Delegate that gets Broadcasted each time the Frequency Spectrum is calculated
|
||||
UPROPERTY(BlueprintAssignable)
|
||||
FFrequencySpectrumCalculated OnFrequencySpectrumCalculated;
|
||||
|
||||
/// Debug
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "SoundVis | Debugging")
|
||||
bool bShowLogDebug;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "SoundVis | Debugging")
|
||||
bool bShowWarningDebug;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly, Category = "SoundVis | Debugging")
|
||||
bool bShowErrorDebug;
|
||||
|
||||
private:
|
||||
|
||||
// The CurrentSoundData is only valid if the AudioPlayer is paused or running!
|
||||
USoundWave* CurrentSoundData;
|
||||
|
||||
// The CurrentSegmentLength is only valid if the AudioPlayer is pause or running
|
||||
float CurrentSegmentLength;
|
||||
|
||||
// Timer and Delegate for the DecompressWorker
|
||||
FTimerHandle AudioDecompressTimer;
|
||||
|
||||
// Timer and Delegate for the auto calculation of the spectrum
|
||||
FTimerHandle FrequencySpectrumTimer;
|
||||
FTimerDelegate FrequencySpectrumTimerDelegate;
|
||||
|
||||
/// FUNCTIONS ///
|
||||
|
||||
public:
|
||||
|
||||
/// De-/Constructors
|
||||
|
||||
// Sets default values for this component's properties
|
||||
USoundVisComponent();
|
||||
|
||||
// Cleans up stuff
|
||||
~USoundVisComponent();
|
||||
|
||||
/// Overrides
|
||||
|
||||
// Tick called each frame
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
/// Functions to load Data from the HardDrive
|
||||
|
||||
// Function to load a sound file from the HD
|
||||
void LoadSoundFileFromHD(const FString& InFilePath);
|
||||
|
||||
// Function to fill in the RawFile sound data into the USoundWave object
|
||||
bool FillSoundWaveInfo(class USoundWave* InSoundWave, TArray<uint8>* InRawFile);
|
||||
|
||||
/// Function to decompress the compressed Data that comes with the .ogg file
|
||||
|
||||
void GetPCMDataFromFile(class USoundWave* InSoundWave);
|
||||
|
||||
/// Function to calculate the frequency spectrum
|
||||
|
||||
void CalculateFrequencySpectrum(USoundWave* InSoundWaveRef, const float InStartTime, const float InDuration, TArray<float>& OutFrequencies);
|
||||
|
||||
/// Helper Functions
|
||||
|
||||
// Function used to get a better value for the FFT. Uses Hann Window
|
||||
float GetFFTInValue(const int16 InSampleValue, const int16 InSampleIndex, const int16 InSampleCount);
|
||||
|
||||
// Function to Start a new DecompressTask
|
||||
void InitNewDecompressTask(USoundWave* InSoundWaveRef);
|
||||
|
||||
// DEBUG Test function to check if Task can call stuff in here
|
||||
void Notify_SoundDecompressed();
|
||||
|
||||
// Function that is looped to handle the calculation of the FrequencySpectrum
|
||||
UFUNCTION()
|
||||
void HandleFrequencySpectrumCalculation();
|
||||
|
||||
/// Blueprint Versions of the File Data Functions
|
||||
|
||||
/**
|
||||
* Will load a file (currently .ogg) from your Hard-Drive and save it in a USoundWave variable
|
||||
*
|
||||
* @param InFilePath Absolute path to the File. E.g.: "C:/Sounds/File.ogg"
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Load Sound File"), Category = "SoundVis | SoundFile")
|
||||
void BP_LoadSoundFileFromHD(const FString InFilePath);
|
||||
|
||||
/**
|
||||
* Will get an Array of Names of the Found SoundFiles
|
||||
*
|
||||
* @param InDirectoryPath Path to the Directory in which the Files are (absolute/relative)
|
||||
* @param bInAbsolutePath Tells if the DirectoryPath is absolute (C:/..) or relative to the GameDirectory
|
||||
* @param InFileExtension This is the Extension the Function should look for. For the Plugin it should be .ogg
|
||||
* @param OutSoundFileNamesWithPath The Array of found SoundFileNames (full Path/Name.Extension)
|
||||
* @param OutSoundFileNamesWithoutPath The Array of found SoundFileNames (only Name.Extension)
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Load Sound File Names"), Category = "SoundVis | SoundFile")
|
||||
void BP_LoadAllSoundFileNamesFromHD(bool& bLoaded, const FString InDirectoryPath, const bool bInAbsolutePath, const FString InFileExtension, TArray<FString>& OutSoundFileNamesWithPath, TArray<FString>& OutSoundFileNamesWithoutPath);
|
||||
|
||||
/**
|
||||
* Will call the CalculateFrequencySpectrum function from BP Side
|
||||
*
|
||||
* @param InSoundWave SoundWave that gets analyzed
|
||||
* @param InStartTime The StartPoint of the TimeWindow we want to analyze
|
||||
* @param InDuration The length of the TimeWindow we want to analyze
|
||||
* @param OutFrequencies Array of float values for x Frequencies from 0 to 22000
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Calculate Freq Spectrum"), Category = "SoundVis | Frequency Spectrum")
|
||||
void BP_CalculateFrequencySpectrum(USoundWave* InSoundWaveRef, const float InStartTime, const float InDuration, TArray<float>& OutFrequencies);
|
||||
|
||||
/**
|
||||
* Will play the passed USoundWave and also start calculating the FrequencySpectrum (loops over the InSegmentLength sized parts)
|
||||
*
|
||||
* @param InSoundWaveRef SoundWave that gets started and analyzed
|
||||
* @param InSegmentLength Length of the SoundWave segment that should get analyzed
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Start Calculate Freq Spectrum"), Category = "SoundVis | Frequency Spectrum")
|
||||
void BP_StartCalculatingFrequencySpectrum(USoundWave* InSoundWaveRef, const float InSegmentLength);
|
||||
|
||||
/**
|
||||
* If playing, pauses the current playing USoundWave and FrequencySpectrum calculation
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Pause Calculate Freq Spectrum"), Category = "SoundVis | Frequency Spectrum")
|
||||
void BP_PauseCalculatingFrequencySpectrum();
|
||||
|
||||
/**
|
||||
* If playing or paused, stops the current USoundWave and FrequencySpectrum calculation
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Stop Calculate Freq Spectrum"), Category = "SoundVis | Frequency Spectrum")
|
||||
void BP_StopCalculatingFrequencySpectrum();
|
||||
|
||||
/**
|
||||
* If paused, resumes the current USoundWave and FrequencySpectrum calculation
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Resume Calculate Freq Spectrum"), Category = "SoundVis | Frequency Spectrum")
|
||||
void BP_ResumeCalculatingFrequencySpectrum();
|
||||
|
||||
/// Sound Player Information
|
||||
|
||||
/**
|
||||
* Returns if the Player is currently playing or not
|
||||
*
|
||||
* @return True if playing
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Is Player Playing"), Category = "SoundVis | SoundPlayer")
|
||||
bool IsPlayerPlaying();
|
||||
|
||||
/**
|
||||
* Returns if the Player is currently paused or not
|
||||
*
|
||||
* @return True if paused
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Is Player Paused"), Category = "SoundVis | SoundPlayer")
|
||||
bool IsPlayerPaused();
|
||||
|
||||
/**
|
||||
* Return the current PlayBack Time of the Sound Player Timer
|
||||
*
|
||||
* @return Current PlayBack Time
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Current Playback Time"), Category = "SoundVis | SoundPlayer")
|
||||
float GetCurrentPlayBackTime();
|
||||
|
||||
/// Frequency Data Functions
|
||||
|
||||
/**
|
||||
* This function will return the value of a specific frequency. It's needs a Frequency Array from the "BP_CalculateFrequencySpectrum" function and the matching SoundWave
|
||||
*
|
||||
* @param InSoundWave SoundWave to get specific data from (SampleRate)
|
||||
* @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "BP_CalculateFrequencySpectrum" function
|
||||
* @param InWantedFrequency The Frequency of which you want the value of
|
||||
* @param OutFrequencyValue Float value of the requested frequency
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Freq Value"), Category = "SoundVis | Frequency Values")
|
||||
static void BP_GetSpecificFrequencyValue(USoundWave* InSoundWave, TArray<float> InFrequencies, int32 InWantedFrequency, float& OutFrequencyValue);
|
||||
|
||||
/**
|
||||
* This function will return the average value for SubBass (20 to 60hz)
|
||||
*
|
||||
* @param InSoundWave SoundWave to get specific data from (SampleRate)
|
||||
* @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "BP_CalculateFrequencySpectrum" function
|
||||
* @param OutAverageSubBass Average value of the frequencies from 20 to 60
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Subbass Value"), Category = "SoundVis | Frequency Values")
|
||||
static void BP_GetAverageSubBassValue(USoundWave* InSoundWave, TArray<float> InFrequencies, float& OutAverageSubBass);
|
||||
|
||||
/**
|
||||
* This function will return the average value for Bass (60 to 250hz)
|
||||
*
|
||||
* @param InSoundWave SoundWave to get specific data from (SampleRate)
|
||||
* @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "BP_CalculateFrequencySpectrum" function
|
||||
* @param OutAverageBass Average value of the frequencies from 60 to 250
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Bass Value"), Category = "SoundVis | Frequency Values")
|
||||
static void BP_GetAverageBassValue(USoundWave* InSoundWave, TArray<float> InFrequencies, float& OutAverageBass);
|
||||
|
||||
/**
|
||||
* This function will return the average value for a given frequency interval e.g.: 20 to 60 (SubBass)
|
||||
*
|
||||
* @param InSoundWave SoundWave to get specific data from (SampleRate)
|
||||
* @param InFrequencies Array of float values for different frequencies from 0 to 22000. Can be get by using the "BP_CalculateFrequencySpectrum" function
|
||||
* @param InStartFrequency Start Frequency of the Frequency interval
|
||||
* @param InEndFrequency End Frequency of the Frequency interval
|
||||
* @param OutAverageFrequency Average value of the requested frequency interval
|
||||
*
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, meta = (DisplayName = "Get Average Freq Value In Range"), Category = "SoundVis | Frequency Values")
|
||||
static void BP_GetAverageFrequencyValueInRange(USoundWave* InSoundWave, TArray<float> InFrequencies, int32 InStartFrequence, int32 InEndFrequence, float& OutAverageFrequency);
|
||||
};
|
||||
12
Plugins/eXiSoundVis/Source/eXiSoundVis/Public/eXiSoundVis.h
Normal file
12
Plugins/eXiSoundVis/Source/eXiSoundVis/Public/eXiSoundVis.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "ModuleManager.h"
|
||||
|
||||
class FeXiSoundVisModule : public IModuleInterface
|
||||
{
|
||||
public:
|
||||
|
||||
/** IModuleInterface implementation */
|
||||
virtual void StartupModule() override;
|
||||
virtual void ShutdownModule() override;
|
||||
};
|
||||
17
Plugins/eXiSoundVis/Source/eXiSoundVis/eXiSoundVis.Build.cs
Normal file
17
Plugins/eXiSoundVis/Source/eXiSoundVis/eXiSoundVis.Build.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using UnrealBuildTool;
|
||||
using System.IO;
|
||||
|
||||
public class eXiSoundVis : ModuleRules
|
||||
{
|
||||
public eXiSoundVis(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PrivateIncludePaths.AddRange(new string[] { "eXiSoundVis/Private" });
|
||||
PublicIncludePaths.AddRange(new string[] { "eXiSoundVis/Public" });
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[] { "Engine", "Core", "CoreUObject", "InputCore", "RHI", "Kiss_FFT" });
|
||||
|
||||
//PublicAdditionalLibraries.Add("legacy_stdio_definitions.lib");
|
||||
|
||||
PublicDependencyModuleNames.Add("Kiss_FFT");
|
||||
}
|
||||
}
|
||||
18
Plugins/eXiSoundVis/eXiSoundVis.uplugin
Normal file
18
Plugins/eXiSoundVis/eXiSoundVis.uplugin
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"FileVersion": 3,
|
||||
"FriendlyName": "eXi's Sound Visualization Plugin",
|
||||
"Version": 1,
|
||||
"VersionName": "1.0",
|
||||
"Description": "This plugin allows you to load .ogg files dynamically from the user's hard drive and then anaylize it. Analyzing it, will return a frequency spectrum for the given Samples. The spectrum can then be used in your project to control all kinds of things.",
|
||||
"Category": "eXi.Sound",
|
||||
"CreatedBy": "Cedric 'eXi' Neukirchen",
|
||||
|
||||
"Modules":
|
||||
[
|
||||
{
|
||||
"Name" : "eXiSoundVis",
|
||||
"Type" : "Runtime",
|
||||
"LoadingPhase" : "PreDefault"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user