// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #include "ImportAudio.h" #if COMPILE_WITH_ASSETS_IMPORTER #include "Engine/Core/DeleteMe.h" #include "Engine/Core/Log.h" #include "Engine/Core/Math/Math.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Serialization/FileReadStream.h" #include "Engine/Serialization/Serialization.h" #include "Engine/Content/Storage/ContentStorageManager.h" #include "Engine/Audio/AudioClip.h" #include "Engine/Tools/AudioTool/AudioTool.h" #include "Engine/Tools/AudioTool/MP3Decoder.h" #include "Engine/Tools/AudioTool/WaveDecoder.h" #include "Engine/Tools/AudioTool/OggVorbisDecoder.h" #include "Engine/Tools/AudioTool/OggVorbisEncoder.h" #include "Engine/Serialization/JsonWriters.h" bool ImportAudio::TryGetImportOptions(const StringView& path, Options& options) { #if IMPORT_AUDIO_CACHE_OPTIONS if (FileSystem::FileExists(path)) { auto tmpFile = ContentStorageManager::GetStorage(path); AssetInitData data; if (tmpFile && tmpFile->GetEntriesCount() == 1 && tmpFile->GetEntry(0).TypeName == AudioClip::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) { // Check import meta rapidjson_flax::Document metadata; metadata.Parse(data.Metadata.Get(), data.Metadata.Length()); if (!metadata.HasParseError()) { options.Deserialize(metadata, nullptr); return true; } } } #endif return false; } CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder& decoder) { // Get import options Options options; if (context.CustomArg != nullptr) { // Copy import options from argument options = *static_cast(context.CustomArg); } else { // Restore the previous settings or use default ones if (!TryGetImportOptions(context.TargetAssetPath, options)) { LOG(Warning, "Missing audio import options. Using default values."); } } // Vorbis uses fixed 16-bit depth if (options.Format == AudioFormat::Vorbis) options.BitDepth = AudioTool::BitDepth::_16; LOG_STR(Info, options.ToString()); // Open the file auto stream = FileReadStream::Open(context.InputPath); if (stream == nullptr) return CreateAssetResult::InvalidPath; DeleteMe deleteStream(stream); // Load the audio data AudioDataInfo info; Array audioData; if (decoder.Convert(stream, info, audioData)) return CreateAssetResult::Error; LOG(Info, "Audio: {0}kHz, channels: {1}, Bit depth: {2}, Length: {3}s", info.SampleRate / 1000.0f, info.NumChannels, info.BitDepth, info.GetLength()); // Load the whole audio data uint32 bytesPerSample = info.BitDepth / 8; uint32 bufferSize = info.NumSamples * bytesPerSample; DataContainer sampleBuffer; sampleBuffer.Link(audioData.Get()); // Convert bit depth if need to uint32 outputBitDepth = (uint32)options.BitDepth; if (outputBitDepth != info.BitDepth) { const uint32 outBufferSize = info.NumSamples * (outputBitDepth / 8); sampleBuffer.Allocate(outBufferSize); AudioTool::ConvertBitDepth(audioData.Get(), info.BitDepth, sampleBuffer.Get(), outputBitDepth, info.NumSamples); info.BitDepth = outputBitDepth; bytesPerSample = info.BitDepth / 8; bufferSize = outBufferSize; } // Base IMPORT_SETUP(AudioClip, AudioClip::SerializedVersion); uint32 samplesPerChunk[ASSET_FILE_DATA_CHUNKS]; Platform::MemoryClear(samplesPerChunk, sizeof(samplesPerChunk)); // Helper macro to write audio data to chunk (handles per chunk compression) #if COMPILE_WITH_OGG_VORBIS #define HANDLE_VORBIS(chunkIndex, dataPtr, dataSize) \ infoEx.NumSamples = samplesPerChunk[chunkIndex]; \ OggVorbisEncoder encoder; \ if (encoder.Convert(dataPtr, infoEx, context.Data.Header.Chunks[chunkIndex]->Data, options.Quality)) \ { \ LOG(Warning, "Failed to compress audio data"); \ return CreateAssetResult::Error; \ } #else #define HANDLE_VORBIS(chunkIndex, dataPtr, dataSize) \ LOG(Warning, "Vorbis format is not supported."); \ return CreateAssetResult::Error; #endif #define HANDLE_RAW(chunkIndex, dataPtr, dataSize) \ context.Data.Header.Chunks[chunkIndex]->Data.Copy(dataPtr, dataSize); #define WRITE_DATA(chunkIndex, dataPtr, dataSize) \ samplesPerChunk[chunkIndex] = (dataSize) / (outputBitDepth / 8); \ switch (options.Format) \ { \ case AudioFormat::Raw: \ { \ HANDLE_RAW(chunkIndex, dataPtr, dataSize); \ } \ break; \ case AudioFormat::Vorbis: \ { \ HANDLE_VORBIS(chunkIndex, dataPtr, dataSize); \ } \ break; \ default: \ { \ LOG(Warning, "Unknown audio format."); \ return CreateAssetResult::Error; \ } \ break; \ } AudioDataInfo infoEx = info; // If audio has streaming disabled then use a single chunk if (options.DisableStreaming) { // Copy data if (context.AllocateChunk(0)) return CreateAssetResult::CannotAllocateChunk; WRITE_DATA(0, sampleBuffer.Get(), bufferSize); } else { // Split audio data into a several chunks (uniform data spread) const int32 minChunkSize = 1 * 1024 * 1024; // 1 MB const int32 dataAlignment = info.NumChannels * bytesPerSample; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes) const int32 chunkSize = Math::Max(minChunkSize, (int32)Math::AlignUp(bufferSize / ASSET_FILE_DATA_CHUNKS, dataAlignment)); const int32 chunksCount = Math::CeilToInt((float)bufferSize / chunkSize); ASSERT(chunksCount > 0 && chunksCount <= ASSET_FILE_DATA_CHUNKS); byte* ptr = sampleBuffer.Get(); int32 size = bufferSize; for (int32 chunkIndex = 0; chunkIndex < chunksCount; chunkIndex++) { if (context.AllocateChunk(chunkIndex)) return CreateAssetResult::CannotAllocateChunk; const int32 t = Math::Min(size, chunkSize); WRITE_DATA(chunkIndex, ptr, t); ptr += t; size -= t; } ASSERT(size == 0); } // Save audio header { AudioClip::Header header; header.Format = options.Format; header.Info = info; header.Is3D = options.Is3D; header.Streamable = !options.DisableStreaming; header.OriginalSize = stream->GetLength(); Platform::MemoryCopy(header.SamplesPerChunk, samplesPerChunk, sizeof(samplesPerChunk)); static_assert(AudioClip::SerializedVersion == 2, "Update this code to match the audio clip header format."); header.ImportedSize = 0; for (int32 i = 0; i < ASSET_FILE_DATA_CHUNKS; i++) { if (context.Data.Header.Chunks[i]) header.ImportedSize += context.Data.Header.Chunks[i]->Size(); } context.Data.CustomData.Copy(&header); } // Create json with import context rapidjson_flax::StringBuffer importOptionsMetaBuffer; importOptionsMetaBuffer.Reserve(256); CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer); JsonWriter& importOptionsMeta = importOptionsMetaObj; importOptionsMeta.StartObject(); { context.AddMeta(importOptionsMeta); options.Serialize(importOptionsMeta, nullptr); } importOptionsMeta.EndObject(); context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); return CreateAssetResult::Ok; } CreateAssetResult ImportAudio::ImportWav(CreateAssetContext& context) { WaveDecoder decoder; return Import(context, decoder); } CreateAssetResult ImportAudio::ImportMp3(CreateAssetContext& context) { MP3Decoder decoder; return Import(context, decoder); } #if COMPILE_WITH_OGG_VORBIS CreateAssetResult ImportAudio::ImportOgg(CreateAssetContext& context) { OggVorbisDecoder decoder; return Import(context, decoder); } #endif #endif