diff --git a/RuntimeAudioImporter/Resources/Icon128.png b/RuntimeAudioImporter/Resources/Icon128.png index d9393a7..85ce4f5 100644 Binary files a/RuntimeAudioImporter/Resources/Icon128.png and b/RuntimeAudioImporter/Resources/Icon128.png differ 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 ... } - ); - } + ); + } }