Files
FlaxEngine/Source/Engine/ContentImporters/ImportAudio.cpp
2024-02-26 19:00:48 +01:00

242 lines
8.1 KiB
C++

// 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<const char>(), 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<Options*>(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<FileReadStream> deleteStream(stream);
// Load the audio data
AudioDataInfo info;
Array<byte> 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<byte> 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<int32>(minChunkSize, (int32)Math::AlignUp<uint32>(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