From f0cc227a330a31d883130759a1b84d0e369a5a7e Mon Sep 17 00:00:00 2001 From: Respirant Date: Sat, 2 Jan 2021 09:53:35 +0300 Subject: [PATCH] big update: callback dispatchers, memory leak fixes and more --- RuntimeAudioImporter/Resources/Icon128.png | Bin 16229 -> 16206 bytes .../RuntimeAudioImporter.uplugin | 24 +- .../Private/RuntimeAudioImporterBPLibrary.cpp | 248 ----------- .../Private/RuntimeAudioImporterLibrary.cpp | 405 ++++++++++++++++++ .../Public/RuntimeAudioImporterBPLibrary.h | 79 ---- .../Public/RuntimeAudioImporterLibrary.h | 175 ++++++++ .../RuntimeAudioImporter.Build.cs | 52 ++- 7 files changed, 608 insertions(+), 375 deletions(-) delete mode 100644 RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterBPLibrary.cpp create mode 100644 RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterLibrary.cpp delete mode 100644 RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterBPLibrary.h create mode 100644 RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterLibrary.h diff --git a/RuntimeAudioImporter/Resources/Icon128.png b/RuntimeAudioImporter/Resources/Icon128.png index d9393a76fba4ec1e8676004727a543e8eea9a175..85ce4f5562b8e19816934b9b8bc5788710c465a4 100644 GIT binary patch delta 14854 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 diff --git a/RuntimeAudioImporter/RuntimeAudioImporter.uplugin b/RuntimeAudioImporter/RuntimeAudioImporter.uplugin index 7802180..094ec1b 100644 --- a/RuntimeAudioImporter/RuntimeAudioImporter.uplugin +++ b/RuntimeAudioImporter/RuntimeAudioImporter.uplugin @@ -4,37 +4,21 @@ "VersionName": "1.0", "FriendlyName": "RuntimeAudioImporter", "Description": "Runtime Audio Importer plugin for Unreal Engine 4. Supports mp3, wav and flac formats.", - "Category": "Code Plugins", + "Category": "Audio", "CreatedBy": "Respirant", "CreatedByURL": "https://github.com/Respirant/RuntimeAudioImporter", "DocsURL": "https://github.com/Respirant/RuntimeAudioImporter", - "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/81fd278e318e4235a028d3fdf46bf036", + "MarketplaceURL": "", "SupportURL": "https://telegram.me/Respirant", "CanContainContent": false, "IsBetaVersion": false, "IsExperimentalVersion": false, "Installed": true, - "EngineVersion": "4.26.0", "Modules": [ { "Name": "RuntimeAudioImporter", "Type": "Runtime", - "LoadingPhase": "Default", - "WhitelistPlatforms": [ - "Win64", - "Win32", - "Mac", - "Android", - "IOS", - "Linux" - ], - "BlacklistPlatforms": [ - "XboxOne", - "PS4", - "HTML5", - "TVOS", - "Switch" - ] + "LoadingPhase": "Default" } ] -} +} \ No newline at end of file diff --git a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterBPLibrary.cpp b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterBPLibrary.cpp deleted file mode 100644 index 11ffb3a..0000000 --- a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterBPLibrary.cpp +++ /dev/null @@ -1,248 +0,0 @@ -// Respirant 2020. - -#include "RuntimeAudioImporterBPLibrary.h" -#include "RuntimeAudioImporter.h" - -URuntimeAudioImporterBPLibrary::URuntimeAudioImporterBPLibrary(const FObjectInitializer& ObjectInitializer) - : Super(ObjectInitializer) -{ - -} - -/** -* Transcode audio file to USoundWave static object -* -* @param filePath Path to the audio file to import -* @param Format Audio file format (extension) -* @param status Final import status (TranscodingStatus) -* @param DefineFormatAutomatically Whether to define format (extension) automatically or not -* @return Returns USoundWave Static Object. -*/ -class USoundWave* URuntimeAudioImporterBPLibrary::GetSoundWaveFromAudioFile(const FString& filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus >& status, bool DefineFormatAutomatically) -{ - return GetUSoundWaveFromAudioFile_Internal(filePath, Format, status, DefineFormatAutomatically); -} - -/** -* Destroy USoundWave static object -* -* @return Returns true - success, false - failure. -*/ -bool URuntimeAudioImporterBPLibrary::DestroySoundWave(USoundWave* ReadySoundWave) { - - if (IsValid(ReadySoundWave) && ReadySoundWave->IsValidLowLevel()) { - - /* Free memory */ - ReadySoundWave->InvalidateCompressedData(); - FMemory::Free(ReadySoundWave->RawPCMData); - ReadySoundWave->RawData.RemoveBulkData(); - ReadySoundWave->RemoveAudioResource(); - /* Free memory */ - - /* Destroying USoundWave object as UObject */ - ReadySoundWave->ConditionalBeginDestroy(); - ReadySoundWave = NULL; - /* Destroying USoundWave object as UObject */ - - return true; - } - else { - return false; - } -} - -/* Get USoundWave static object reference */ -class USoundWave* URuntimeAudioImporterBPLibrary::GetSoundWaveObject(const uint8* WaveData, int32 WaveDataSize, TEnumAsByte < TranscodingStatus >& status) { - USoundWave* sw = NewObject(USoundWave::StaticClass()); - if (!sw) { - status = TranscodingStatus::USoundWaveDeclarationError; - return nullptr; - } - FWaveModInfo WaveInfo; - FString ErrorReason; - if (WaveInfo.ReadWaveInfo(WaveData, WaveDataSize, &ErrorReason)) - { - if (*WaveInfo.pBitsPerSample != 16) { - status = TranscodingStatus::UnsupportedBitDepth; - return nullptr; - } - sw->InvalidateCompressedData(); - sw->RawData.Lock(LOCK_READ_WRITE); - void* LockedData = sw->RawData.Realloc(WaveDataSize); - FMemory::Memcpy(LockedData, WaveData, WaveDataSize); - sw->RawData.Unlock(); - - int32 DurationDiv = *WaveInfo.pChannels * *WaveInfo.pBitsPerSample * *WaveInfo.pSamplesPerSec; - if (DurationDiv) - { - sw->Duration = *WaveInfo.pWaveDataSize * 8.0f / DurationDiv; - } - else - { - sw->Duration = 0.0f; - status = TranscodingStatus::ZeroDurationError; - return nullptr; - } - sw->SetSampleRate(*WaveInfo.pSamplesPerSec); - sw->NumChannels = *WaveInfo.pChannels; - sw->RawPCMDataSize = WaveInfo.SampleDataSize; - sw->SoundGroup = ESoundGroup::SOUNDGROUP_Default; - } - else { - if (ErrorReason.Equals(TEXT("Invalid WAVE file."), ESearchCase::CaseSensitive)) { - status = TranscodingStatus::InvalidWAVEData; - } - else if (ErrorReason.Equals(TEXT("Unsupported WAVE file format: actual bit rate does not match the container size."), ESearchCase::CaseSensitive)) { - status = TranscodingStatus::InvalidBitrate; - } - else if (ErrorReason.Equals(TEXT("Unsupported WAVE file format: subformat identifier not recognized."), ESearchCase::CaseSensitive)) { - status = TranscodingStatus::InvalidBitrate; - } - else { - status = TranscodingStatus::UnrecognizedReadWaveError; - } - return nullptr; - } - - sw->RawPCMData = (uint8*)FMemory::Malloc(sw->RawPCMDataSize); - FMemory::Memcpy(sw->RawPCMData, WaveInfo.SampleDataStart, sw->RawPCMDataSize); - - if (!sw) { - status = TranscodingStatus::InvalidUSoundWave; - return nullptr; - } - status = TranscodingStatus::SuccessTranscoding; - return sw; -} - -#include "ThirdParty/dr_wav.h" - -#include "ThirdParty/dr_mp3.h" - -#include "ThirdParty/dr_flac.h" - -/* Main internal get USoundWave from audio file */ -class USoundWave* URuntimeAudioImporterBPLibrary::GetUSoundWaveFromAudioFile_Internal(const FString& filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus >& status, bool DefineFormatAutomatically) -{ - if (FPaths::FileExists(filePath) != true) { - status = TranscodingStatus::AudioDoesNotExist; - return nullptr; - } - - int16_t* pSampleData; - uint64 framesToWrite; - uint32 channels; - uint32 sampleRate; - - if (DefineFormatAutomatically) { - Format = GetAudioFormat(filePath); - if (Format == AudioFormat::INVALID) { - status = TranscodingStatus::InvalidAudioFormat; - return nullptr; - } - } - if (TranscodeAudioToWaveData(TCHAR_TO_ANSI(*filePath), Format, status, framesToWrite, pSampleData, channels, sampleRate) == false) { - return nullptr; - } - - drwav wavEncode; - drwav_data_format format; - format.container = drwav_container_riff; - format.format = DR_WAVE_FORMAT_PCM; - format.channels = channels; - format.sampleRate = sampleRate; - format.bitsPerSample = 16; // Unreal Engine 4 supports only 16 bit rate - - void* pWavFileData; - size_t wavFileSize; - drwav_init_memory_write(&wavEncode, &pWavFileData, &wavFileSize, &format, NULL); - drwav_write_pcm_frames(&wavEncode, framesToWrite, pSampleData); - drwav_uninit(&wavEncode); - - USoundWave* ReadyUSoundWave = GetSoundWaveObject((const uint8*)pWavFileData, wavFileSize, status); // A ready USoundWave static object - - drwav_free(pSampleData, NULL); - drwav_free(pWavFileData, NULL); - - return ReadyUSoundWave; -} - -/* Automatically get audio format. Internal use only recommended */ -TEnumAsByte URuntimeAudioImporterBPLibrary::GetAudioFormat(const FString& filePath) { - if (FPaths::GetExtension(filePath, false).Equals(TEXT("mp3"), ESearchCase::IgnoreCase)) { - return AudioFormat::MP3; - } - else if (FPaths::GetExtension(filePath, false).Equals(TEXT("wav"), ESearchCase::IgnoreCase)) { - return AudioFormat::WAV; - } - else if (FPaths::GetExtension(filePath, false).Equals(TEXT("flac"), ESearchCase::IgnoreCase)) { - return AudioFormat::FLAC; - } - else { - return AudioFormat::INVALID; - } -} - -/* Transcode Audio to Wave Data */ -bool URuntimeAudioImporterBPLibrary::TranscodeAudioToWaveData(const char* filePath, TEnumAsByte Format, TEnumAsByte& status, uint64& framesToWrite, int16_t*& pSampleData, uint32& channels, uint32& sampleRate) { - switch (Format) - { - case AudioFormat::MP3: - { - drmp3 mp3; - if (!drmp3_init_file(&mp3, filePath, NULL)) { - status = TranscodingStatus::FailedToOpenStream; - return false; - } - else { - pSampleData = (int16_t*)malloc((size_t)drmp3_get_pcm_frame_count(&mp3) * mp3.channels * sizeof(int16_t)); - framesToWrite = drmp3_read_pcm_frames_s16(&mp3, drmp3_get_pcm_frame_count(&mp3), pSampleData); - channels = mp3.channels; - sampleRate = mp3.sampleRate; - drmp3_uninit(&mp3); - return true; - } - break; - } - case AudioFormat::WAV: - { - drwav wav; - if (!drwav_init_file(&wav, filePath, NULL)) { - status = TranscodingStatus::FailedToOpenStream; - return false; - } - else { - pSampleData = (int16_t*)malloc((size_t)wav.totalPCMFrameCount * wav.channels * sizeof(int16_t)); - framesToWrite = drwav_read_pcm_frames_s16(&wav, wav.totalPCMFrameCount, pSampleData); - channels = wav.channels; - sampleRate = wav.sampleRate; - drwav_uninit(&wav); - return true; - } - break; - } - case AudioFormat::FLAC: - { - drflac* pFlac = drflac_open_file(filePath, NULL); - if (pFlac == NULL) { - status = TranscodingStatus::FailedToOpenStream; - return false; - } - else { - pSampleData = (int16_t*)malloc((size_t)pFlac->totalPCMFrameCount * pFlac->channels * sizeof(int16_t)); - framesToWrite = drflac_read_pcm_frames_s16(pFlac, pFlac->totalPCMFrameCount, pSampleData); - channels = pFlac->channels; - sampleRate = pFlac->sampleRate; - drflac_close(pFlac); - return true; - } - break; - } - default: - { - status = TranscodingStatus::InvalidAudioFormat; - return false; - break; - } - } -} diff --git a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterLibrary.cpp b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterLibrary.cpp new file mode 100644 index 0000000..18116dc --- /dev/null +++ b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Private/RuntimeAudioImporterLibrary.cpp @@ -0,0 +1,405 @@ +// Respirant 2020. + +#include "RuntimeAudioImporterLibrary.h" +#include "RuntimeAudioImporter.h" + +URuntimeAudioImporterLibrary::URuntimeAudioImporterLibrary(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + +} + +URuntimeAudioImporterLibrary::~URuntimeAudioImporterLibrary() +{ +} + +URuntimeAudioImporterLibrary* URuntimeAudioImporterLibrary::CreateRuntimeAudioImporter() +{ + URuntimeAudioImporterLibrary* Importer = NewObject(); + return Importer; +} + +URuntimeAudioImporterLibrary* URuntimeAudioImporterLibrary::ImportAudioFromFile(const FString& filePath, TEnumAsByte < AudioFormat > Format, bool DefineFormatAutomatically) +{ + AddToRoot(); + AsyncTask(ENamedThreads::AnyThread, [=]() { + GetUSoundWaveFromAudioFile_Internal(filePath, Format, DefineFormatAutomatically); + }); + return this; +} + +bool URuntimeAudioImporterLibrary::DestroySoundWave(USoundWave* ReadySoundWave) { + + if (ReadySoundWave->IsValidLowLevel()) { + + //Begin USoundWave destroy (block changing some data) + ReadySoundWave->ConditionalBeginDestroy(); + ReadySoundWave->BeginDestroy(); + + //Delete all audio data from physical memory + ReadySoundWave->RawData.RemoveBulkData(); + ReadySoundWave->RemoveAudioResource(); + ReadySoundWave->InvalidateCompressedData(true); + + //Finish USoundWave destroy (complete object deletion as UObject) + if (ReadySoundWave->IsReadyForFinishDestroy()) { + ReadySoundWave->ConditionalFinishDestroy(); + ReadySoundWave->FinishDestroy(); + return true; + } + else { + return false; + } + } + else { + return false; + } +} + +/** + * Including dr_wav, dr_mp3 and dr_flac libraries + */ +#include "ThirdParty/dr_wav.h" +#include "ThirdParty/dr_mp3.h" +#include "ThirdParty/dr_flac.h" + + /** + * Replacing standard CPP memory functions (malloc, realloc, free) with memory management functions that are maintained by Epic as the engine evolves. + */ +void* unreal_malloc(size_t sz, void* pUserData) +{ + return FMemory::Malloc(sz); +} +void* unreal_realloc(void* p, size_t sz, void* pUserData) +{ + return FMemory::Realloc(p, sz); +} +void unreal_free(void* p, void* pUserData) +{ + FMemory::Free(p); +} + +URuntimeAudioImporterLibrary* URuntimeAudioImporterLibrary::GetUSoundWaveFromAudioFile_Internal(const FString& filePath, TEnumAsByte < AudioFormat > Format, bool DefineFormatAutomatically) +{ + TEnumAsByte < TranscodingStatus > Status; + if (FPaths::FileExists(filePath) != true) { + Status = TranscodingStatus::AudioDoesNotExist; + + // Callback Dispatcher OnResult + OnResult_Internal(nullptr, Status); + } + else { + int16_t* pSampleData; + uint64 framesToWrite; + uint32 channels; + uint32 sampleRate; + + if (DefineFormatAutomatically) { + Format = GetAudioFormat(filePath); + } + + if (Format == AudioFormat::INVALID) { + Status = TranscodingStatus::InvalidAudioFormat; + + // Callback Dispatcher OnResult + OnResult_Internal(nullptr, Status); + } + else { + if (TranscodeAudioFileToPCMData(this, TCHAR_TO_ANSI(*filePath), Format, Status, framesToWrite, pSampleData, channels, sampleRate) == false) { + FMemory::Free(pSampleData); + + // Callback Dispatcher OnResult + OnResult_Internal(nullptr, Status); + } + else { + void* WaveData; + size_t WaveDataSize; + + if (TranscodePCMToWAVData(this, *&WaveData, *&WaveDataSize, framesToWrite, *&pSampleData, channels, sampleRate) == false) { + FMemory::Free(pSampleData); + + // Callback Dispatcher OnResult + OnResult_Internal(nullptr, Status); + } + else { + USoundWave* ReadyUSoundWave = GetSoundWaveObject(this, (const uint8*)WaveData, WaveDataSize, Status); // A ready USoundWave static object + + /* Free temporary transcoding data */ + FMemory::Free(WaveData); + FMemory::Free(pSampleData); + /* Free temporary transcoding data */ + + // Callback Dispatcher OnProgress (not completely accurate implementation) + OnProgress_Internal(100); + + // Callback Dispatcher OnResult + OnResult_Internal(ReadyUSoundWave, Status); + } + + + } + } + } + return this; +} + +class USoundWave* URuntimeAudioImporterLibrary::GetSoundWaveObject(URuntimeAudioImporterLibrary* RuntimeAudioImporterObjRef, const uint8* WaveData, int32 WaveDataSize, TEnumAsByte < TranscodingStatus >& Status) +{ + USoundWave* sw = NewObject(USoundWave::StaticClass()); + if (!sw) { + Status = TranscodingStatus::USoundWaveDeclarationError; + return nullptr; + } + FWaveModInfo WaveInfo; + FString ErrorReason; + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(65); + + if (WaveInfo.ReadWaveInfo(WaveData, WaveDataSize, &ErrorReason)) + { + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(75); + + if (*WaveInfo.pBitsPerSample != 16) { + Status = TranscodingStatus::UnsupportedBitDepth; + return nullptr; + } + sw->InvalidateCompressedData(); + sw->RawData.Lock(LOCK_READ_WRITE); + void* LockedData = sw->RawData.Realloc(WaveDataSize); + FMemory::Memcpy(LockedData, WaveData, WaveDataSize); + sw->RawData.Unlock(); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(80); + + int32 DurationDiv = *WaveInfo.pChannels * *WaveInfo.pBitsPerSample * *WaveInfo.pSamplesPerSec; + if (DurationDiv) + { + sw->Duration = *WaveInfo.pWaveDataSize * 8.0f / DurationDiv; + } + else + { + sw->Duration = 0.0f; + Status = TranscodingStatus::ZeroDurationError; + return nullptr; + } + sw->SetSampleRate(*WaveInfo.pSamplesPerSec); + sw->NumChannels = *WaveInfo.pChannels; + sw->RawPCMDataSize = WaveInfo.SampleDataSize; + sw->SoundGroup = ESoundGroup::SOUNDGROUP_Default; + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(85); + } + else { + if (ErrorReason.Equals(TEXT("Invalid WAVE file."), ESearchCase::CaseSensitive)) { + Status = TranscodingStatus::InvalidWAVEData; + } + else if (ErrorReason.Equals(TEXT("Unsupported WAVE file format: actual bit rate does not match the container size."), ESearchCase::CaseSensitive)) { + Status = TranscodingStatus::InvalidBitrate; + } + else if (ErrorReason.Equals(TEXT("Unsupported WAVE file format: subformat identifier not recognized."), ESearchCase::CaseSensitive)) { + Status = TranscodingStatus::InvalidBitrate; + } + else { + Status = TranscodingStatus::UnrecognizedReadWaveError; + } + return nullptr; + } + + sw->RawPCMData = (uint8*)FMemory::Malloc(sw->RawPCMDataSize); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(90); + + FMemory::Memcpy(sw->RawPCMData, WaveInfo.SampleDataStart, sw->RawPCMDataSize); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(95); + + if (!sw) { + Status = TranscodingStatus::InvalidUSoundWave; + return nullptr; + } + Status = TranscodingStatus::SuccessTranscoding; + return sw; +} + +bool URuntimeAudioImporterLibrary::TranscodeAudioFileToPCMData(URuntimeAudioImporterLibrary* RuntimeAudioImporterObjRef, const char* filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus >& Status, uint64& framesToWrite, int16_t*& pSampleData, uint32& channels, uint32& sampleRate) +{ + RuntimeAudioImporterObjRef->OnProgress_Internal(5); + switch (Format) + { + case AudioFormat::MP3: + { + drmp3_allocation_callbacks allocationCallbacksDecoding; + allocationCallbacksDecoding.pUserData = NULL; + allocationCallbacksDecoding.onMalloc = unreal_malloc; + allocationCallbacksDecoding.onRealloc = unreal_realloc; + allocationCallbacksDecoding.onFree = unreal_free; + + drmp3 mp3; + if (!drmp3_init_file(&mp3, filePath, &allocationCallbacksDecoding)) { + Status = TranscodingStatus::FailedToOpenStream; + return false; + } + else { + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(10); + + pSampleData = (int16_t*)FMemory::Malloc((size_t)drmp3_get_pcm_frame_count(&mp3) * mp3.channels * sizeof(int16_t)); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(15); + + framesToWrite = drmp3_read_pcm_frames_s16(&mp3, drmp3_get_pcm_frame_count(&mp3), pSampleData); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(35); + + channels = mp3.channels; + sampleRate = mp3.sampleRate; + drmp3_uninit(&mp3); + return true; + } + break; + } + case AudioFormat::WAV: + { + drwav_allocation_callbacks allocationCallbacksDecoding; + allocationCallbacksDecoding.pUserData = NULL; + allocationCallbacksDecoding.onMalloc = unreal_malloc; + allocationCallbacksDecoding.onRealloc = unreal_realloc; + allocationCallbacksDecoding.onFree = unreal_free; + + drwav wav; + if (!drwav_init_file(&wav, filePath, &allocationCallbacksDecoding)) { + Status = TranscodingStatus::FailedToOpenStream; + return false; + } + else { + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(10); + + pSampleData = (int16_t*)FMemory::Malloc((size_t)wav.totalPCMFrameCount * wav.channels * sizeof(int16_t)); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(15); + + framesToWrite = drwav_read_pcm_frames_s16(&wav, wav.totalPCMFrameCount, pSampleData); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(35); + + channels = wav.channels; + sampleRate = wav.sampleRate; + drwav_uninit(&wav); + return true; + } + break; + } + case AudioFormat::FLAC: + { + drflac_allocation_callbacks allocationCallbacksDecoding; + allocationCallbacksDecoding.pUserData = NULL; + allocationCallbacksDecoding.onMalloc = unreal_malloc; + allocationCallbacksDecoding.onRealloc = unreal_realloc; + allocationCallbacksDecoding.onFree = unreal_free; + + drflac* pFlac = drflac_open_file(filePath, &allocationCallbacksDecoding); + if (pFlac == NULL) { + Status = TranscodingStatus::FailedToOpenStream; + return false; + } + else { + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(10); + + pSampleData = (int16_t*)FMemory::Malloc((size_t)pFlac->totalPCMFrameCount * pFlac->channels * sizeof(int16_t)); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(15); + + framesToWrite = drflac_read_pcm_frames_s16(pFlac, pFlac->totalPCMFrameCount, pSampleData); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(35); + + channels = pFlac->channels; + sampleRate = pFlac->sampleRate; + drflac_close(pFlac); + return true; + } + break; + } + default: + { + Status = TranscodingStatus::InvalidAudioFormat; + return false; + break; + } + } +} + +bool URuntimeAudioImporterLibrary::TranscodePCMToWAVData(URuntimeAudioImporterLibrary* RuntimeAudioImporterObjRef, void*& WaveData, size_t& WaveDataSize, uint64 framesToWrite, int16_t*& pSampleData, uint32 channels, uint32 sampleRate) { + drwav wavEncode; + drwav_data_format format; + format.container = drwav_container_riff; + format.format = DR_WAVE_FORMAT_PCM; + format.channels = channels; + format.sampleRate = sampleRate; + format.bitsPerSample = 16; // Unreal Engine supports only 16-bit rate + + drwav_allocation_callbacks allocationCallbacks; + allocationCallbacks.pUserData = NULL; + allocationCallbacks.onMalloc = unreal_malloc; + allocationCallbacks.onRealloc = unreal_realloc; + allocationCallbacks.onFree = unreal_free; + if (!drwav_init_memory_write(&wavEncode, &WaveData, &WaveDataSize, &format, &allocationCallbacks)) { + return false; //error + } + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(40); + + drwav_write_pcm_frames(&wavEncode, framesToWrite, pSampleData); + + // Callback Dispatcher OnProgress (not completely accurate implementation) + RuntimeAudioImporterObjRef->OnProgress_Internal(60); + + drwav_uninit(&wavEncode); + return true; +} + +TEnumAsByte URuntimeAudioImporterLibrary::GetAudioFormat(const FString& filePath) +{ + if (FPaths::GetExtension(filePath, false).Equals(TEXT("mp3"), ESearchCase::IgnoreCase)) { + return AudioFormat::MP3; + } + else if (FPaths::GetExtension(filePath, false).Equals(TEXT("wav"), ESearchCase::IgnoreCase)) { + return AudioFormat::WAV; + } + else if (FPaths::GetExtension(filePath, false).Equals(TEXT("flac"), ESearchCase::IgnoreCase)) { + return AudioFormat::FLAC; + } + else { + return AudioFormat::INVALID; + } +} + +void URuntimeAudioImporterLibrary::OnProgress_Internal(int32 Percentage) +{ + AsyncTask(ENamedThreads::GameThread, [=]() { + OnProgress.Broadcast(Percentage); + }); +} + +void URuntimeAudioImporterLibrary::OnResult_Internal(USoundWave* ReadySoundWave, TEnumAsByte < TranscodingStatus > Status) +{ + AsyncTask(ENamedThreads::GameThread, [=]() { + RemoveFromRoot(); + OnResult.Broadcast(ReadySoundWave, Status); + }); +} diff --git a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterBPLibrary.h b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterBPLibrary.h deleted file mode 100644 index af172c5..0000000 --- a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterBPLibrary.h +++ /dev/null @@ -1,79 +0,0 @@ -// Respirant 2020. - -#pragma once - -#include "Kismet/BlueprintFunctionLibrary.h" - -#include "Sound/SoundWave.h" - -#include "Developer/TargetPlatform/Public/Interfaces/IAudioFormat.h" - -#include "RuntimeAudioImporterBPLibrary.generated.h" - - -UENUM(BlueprintType) -enum TranscodingStatus -{ - SuccessTranscoding UMETA(DisplayName = "Success"), - FailedToOpenStream UMETA(DisplayName = "Failed to open stream"), - UnsupportedBitDepth UMETA(DisplayName = "Unsupported bit depth (Unreal Engine only supports 16 bit)"), - InvalidWAVEData UMETA(DisplayName = "Invalid WAVE data"), - InvalidBitrate UMETA(DisplayName = "Invalid bitrate (Actual bit rate does not match the container size)"), - InvalidSubformat UMETA(DisplayName = "InvalidSubformat (Subformat identifier not recognized)"), - UnrecognizedReadWaveError UMETA(DisplayName = "Unrecognized ReadWave error"), - USoundWaveDeclarationError UMETA(DisplayName = "USoundWave declaration error"), - InvalidUSoundWave UMETA(DisplayName = "Invalid USoundWave (The USoundWave object contains incorrect data)"), - InvalidAudioFormat UMETA(DisplayName = "Invalid audio format (Can't determine the format of the audio file)"), - AudioDoesNotExist UMETA(DisplayName = "The audio file does not exist"), - ZeroDurationError UMETA(DisplayName = "Transcoded audio has zero length") -}; - -UENUM(BlueprintType) -enum AudioFormat -{ - MP3 UMETA(DisplayName = "mp3"), - WAV UMETA(DisplayName = "wav"), - FLAC UMETA(DisplayName = "flac"), - INVALID UMETA(DisplayName = "invalid (not defined format, internal use only)", Hidden) -}; - -UCLASS() -class RUNTIMEAUDIOIMPORTER_API URuntimeAudioImporterBPLibrary : public UBlueprintFunctionLibrary -{ - GENERATED_UCLASS_BODY() -public: - - /** - * Transcode audio file to USoundWave static object - * - * @param filePath Path to the audio file to import - * @param Format Audio file format (extension) - * @param status Final import status (TranscodingStatus) - * @param DefineFormatAutomatically Whether to define format (extension) automatically or not - * @return Returns USoundWave Static Object. - */ - UFUNCTION(BlueprintCallable, meta = (Keywords = "Importer, Transcoder, Converter, MP3, FLAC, WAV", ToolTip = "Transcode audio file to USoundWave static object"), Category = "RuntimeAudioImporter") - static class USoundWave* GetSoundWaveFromAudioFile(const FString & filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus > & status, bool DefineFormatAutomatically); - - /** - * Destroy USoundWave static object - * - * @return Returns true - success, false - failure. - */ - UFUNCTION(BlueprintCallable, meta = (Keywords = "Importer, Transcoder, Converter, MP3, FLAC, WAV, Destroy", ToolTip = "After the Soundwave is no longer needed, you need to call this function to free memory"), Category = "RuntimeAudioImporter") - static bool DestroySoundWave(USoundWave * ReadySoundWave); - -private: - - /* Main internal get USoundWave from audio file */ - static class USoundWave* GetUSoundWaveFromAudioFile_Internal(const FString & filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus > & status, bool DefineFormatAutomatically); - - /* Get USoundWave static object reference */ - static class USoundWave* GetSoundWaveObject(const uint8 * WaveData, int32 WaveDataSize, TEnumAsByte < TranscodingStatus > & status); - - /* Transcode Audio to Wave Data */ - static bool TranscodeAudioToWaveData(const char* filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus > & status, uint64 & framesToWrite, int16_t * &pSampleData, uint32 & channels, uint32 & sampleRate); - - /* Automatically get audio format. Internal use only recommended */ - static TEnumAsByte GetAudioFormat(const FString & filePath); -}; diff --git a/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterLibrary.h b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterLibrary.h new file mode 100644 index 0000000..f19acb0 --- /dev/null +++ b/RuntimeAudioImporter/Source/RuntimeAudioImporter/Public/RuntimeAudioImporterLibrary.h @@ -0,0 +1,175 @@ +// Respirant 2020. +#pragma once + +#include "Sound/SoundWave.h" +//#include "Developer/TargetPlatform/Public/Interfaces/IAudioFormat.h" +#include "Async/Async.h" +#include "RuntimeAudioImporterLibrary.generated.h" + +/** + * Possible audio importing results + */ +UENUM(BlueprintType, Category = "RuntimeAudioImporter") +enum TranscodingStatus +{ + SuccessTranscoding UMETA(DisplayName = "Success"), + FailedToOpenStream UMETA(DisplayName = "Failed to open stream"), + UnsupportedBitDepth UMETA(DisplayName = "Unsupported bit depth (Unreal Engine only supports 16 bit)"), + InvalidWAVEData UMETA(DisplayName = "Invalid WAVE data"), + InvalidBitrate UMETA(DisplayName = "Invalid bitrate (Actual bit rate does not match the container size)"), + InvalidSubformat UMETA(DisplayName = "InvalidSubformat (Subformat identifier not recognized)"), + UnrecognizedReadWaveError UMETA(DisplayName = "Unrecognized ReadWave error"), + USoundWaveDeclarationError UMETA(DisplayName = "USoundWave declaration error"), + InvalidUSoundWave UMETA(DisplayName = "Invalid USoundWave (The USoundWave object contains incorrect data)"), + InvalidAudioFormat UMETA(DisplayName = "Invalid audio format (Can't determine the format of the audio file)"), + AudioDoesNotExist UMETA(DisplayName = "The audio file does not exist"), + ZeroDurationError UMETA(DisplayName = "Transcoded audio has zero length") +}; + +/** + * Possible audio formats + */ +UENUM(BlueprintType, Category = "RuntimeAudioImporter") +enum AudioFormat +{ + MP3 UMETA(DisplayName = "mp3"), + WAV UMETA(DisplayName = "wav"), + FLAC UMETA(DisplayName = "flac"), + INVALID UMETA(DisplayName = "invalid (not defined format, internal use only)", Hidden) +}; + +/** + * Declare delegate which will be called during the transcoding process + * + * @param Percentage Percentage of download completed (0-100%) + * @note The impementation of dispatcher is not fully right. The data is taken approximately, and will not show exactly the correct percentage of transcoding progress + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnProgress, const int32, Percentage); + +/** + * Declare a delegate that will be called on the transcoding result + * + * @param ReadySoundWave Ready USoundWave Object + * @param Status Reference to TranscodingStatus Enumerator in case an error occurs + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnResult, const USoundWave*, ReadySoundWave, const TEnumAsByte < TranscodingStatus >, Status); + +UCLASS(BlueprintType, Category = "RuntimeFilesDownloader") +class RUNTIMEAUDIOIMPORTER_API URuntimeAudioImporterLibrary : public UObject +{ + GENERATED_UCLASS_BODY() +public: + /** + * Bind to know when the transcoding is on progress. + */ + UPROPERTY(BlueprintAssignable, Category = "RuntimeAudioImporter") + FOnProgress OnProgress; + + /** + * Bind to know when the transcoding is complete (even if it fails). + */ + UPROPERTY(BlueprintAssignable, Category = "RuntimeAudioImporter") + FOnResult OnResult; + + URuntimeAudioImporterLibrary(); + ~URuntimeAudioImporterLibrary(); + + /** + * Instantiates a RuntimeAudioImporter object. + * + * @return The RuntimeAudioImporter object. Bind to it's OnProgress and OnResult events to know when it is in the process of transcoding and has been transcoded. + */ + UFUNCTION(BlueprintCallable, meta = (Keywords = "Create, Audio, Runtime, MP3, FLAC, WAV"), Category = "RuntimeAudioImporter") + static URuntimeAudioImporterLibrary * CreateRuntimeAudioImporter(); + + /** + * Transcode audio file to USoundWave static object + * + * @param filePath Path to the audio file to import + * @param Format Audio file format (extension) + * @param DefineFormatAutomatically Whether to define format (extension) automatically or not + * @return Returns itself. + */ + UFUNCTION(BlueprintCallable, meta = (Keywords = "Importer, Transcoder, Converter, Runtime, MP3, FLAC, WAV"), Category = "RuntimeAudioImporter") + URuntimeAudioImporterLibrary * ImportAudioFromFile(const FString & filePath, TEnumAsByte < AudioFormat > Format, bool DefineFormatAutomatically); + + /** + * Destroy USoundWave static object. After the Soundwave is no longer needed, you need to call this function to free memory + * + * @param ReadySoundWave SoundWave Object need to be destroyed + * @return Whether the descruction of USoundWave was successfull or not + */ + UFUNCTION(BlueprintCallable, meta = (Keywords = "MP3, FLAC, WAV, Destroy"), Category = "RuntimeAudioImporter") + static bool DestroySoundWave(USoundWave * ReadySoundWave); + +private: + /** + * Internal main audio transcoding function + * + * @param filePath Path to the audio file to import + * @param Format Audio file format (extension) + * @param DefineFormatAutomatically Whether to define format (extension) automatically or not + * @return Returns itself + */ + URuntimeAudioImporterLibrary * GetUSoundWaveFromAudioFile_Internal(const FString & filePath, TEnumAsByte < AudioFormat > Format, bool DefineFormatAutomatically); + + /** + * Get USoundWave static object reference from 16-bit WAV data + * + * @param RuntimeAudioImporterObjRef Reference to the created RuntimeAudioImporter object + * @param WaveData Reference to memory location of 16-bit WAV byte data + * @param WaveDataSize Memory size allocated for 16-bit WAV byte data + * @param Status Reference to TranscodingStatus Enumerator in case an error occurs + * @return Returns a ready USoundWave object + */ + static class USoundWave* GetSoundWaveObject(URuntimeAudioImporterLibrary * RuntimeAudioImporterObjRef, const uint8 * WaveData, int32 WaveDataSize, TEnumAsByte < TranscodingStatus > & Status); + + /** + * Transcode Audio from File to PCM Data + * + * @param RuntimeAudioImporterObjRef Reference to the created RuntimeAudioImporter object + * @param filePath Path to the audio file from which to receive PCM data + * @param Format Format of the audio file (e.g. mp3. flac, etc) + * @param Status Reference to TranscodingStatus Enumerator in case an error occurs + * @param framesToWrite PCM frames ready to be written + * @param pSampleData Ready PCM sample data for which memory has been allocated + * @param channels Quantity of channels (e.g. 1, 2, etc) + * @param sampleRate Sample rate of the audio + * @return Whether the transcoding was successful or not + */ + static bool TranscodeAudioFileToPCMData(URuntimeAudioImporterLibrary * RuntimeAudioImporterObjRef, const char* filePath, TEnumAsByte < AudioFormat > Format, TEnumAsByte < TranscodingStatus > & Status, uint64 & framesToWrite, int16_t * &pSampleData, uint32 & channels, uint32 & sampleRate); + + /** + * Transcode Audio from PCM to 16-bit WAV data + * + * @param RuntimeAudioImporterObjRef Reference to the created RuntimeAudioImporter object + * @param WaveData Reference to memory location of 16-bit WAV byte data + * @param WaveDataSize Memory size allocated for 16-bit WAV byte data + * @param framesToWrite PCM frames which will be written for WAV data + * @param pSampleData Ready WAV sample data for which memory has been allocated + * @param channels Quantity of channels (e.g. 1, 2, etc) + * @param sampleRate Sample rate of the audio + * @return Whether the transcoding was successful or not + */ + static bool TranscodePCMToWAVData(URuntimeAudioImporterLibrary* RuntimeAudioImporterObjRef, void*& WaveData, size_t & WaveDataSize, uint64 framesToWrite, int16_t * &pSampleData, uint32 channels, uint32 sampleRate); + + /** + * @param filePath File path, where to find the format (by extension) + * @return Returns the found audio format (e.g. mp3. flac, etc) by AudioFormat Enumerator + */ + static TEnumAsByte GetAudioFormat(const FString & filePath); + + /** + * Audio transcoding progress callback + * @param Percentage Percentage of download completed (0-100%) + * @note The impementation of dispatcher is not fully right. The data is taken approximately, and will not show exactly the correct percentage of transcoding progress + */ + void OnProgress_Internal(int32 Percentage); + + /** + * Audio transcoding finished callback + * @param ReadySoundWave A ready USoundWave object + * @param Status Reference to TranscodingStatus Enumerator + */ + void OnResult_Internal(USoundWave * ReadySoundWave, TEnumAsByte < TranscodingStatus > Status); +}; diff --git a/RuntimeAudioImporter/Source/RuntimeAudioImporter/RuntimeAudioImporter.Build.cs b/RuntimeAudioImporter/Source/RuntimeAudioImporter/RuntimeAudioImporter.Build.cs index 1dcbb9f..3e24567 100644 --- a/RuntimeAudioImporter/Source/RuntimeAudioImporter/RuntimeAudioImporter.Build.cs +++ b/RuntimeAudioImporter/Source/RuntimeAudioImporter/RuntimeAudioImporter.Build.cs @@ -1,15 +1,13 @@ // Respirant 2020. - using System; using System.IO; using UnrealBuildTool; public class RuntimeAudioImporter : ModuleRules { - public RuntimeAudioImporter(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - + public RuntimeAudioImporter(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; PublicDefinitions.AddRange( new string[] { @@ -19,35 +17,33 @@ public class RuntimeAudioImporter : ModuleRules } ); - //PrivateIncludePaths.Add(Path.Combine(ModuleDirectory, "ThirdParty")); - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", + new string[] + { + "Core", // ... add other public dependencies that you statically link with here ... } - ); - - - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore", + ); + + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", // ... add private dependencies that you statically link with here ... } - ); - - - DynamicallyLoadedModuleNames.AddRange( - new string[] - { + ); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { // ... add any modules that your module loads dynamically here ... } - ); - } + ); + } }