Files
FlaxEngine/Source/Engine/Audio/AudioClip.cpp
2025-08-11 18:42:55 +02:00

502 lines
14 KiB
C++

// Copyright (c) Wojciech Figat. All rights reserved.
#include "AudioClip.h"
#include "Audio.h"
#include "AudioSource.h"
#include "AudioBackend.h"
#include "Engine/Core/Log.h"
#include "Engine/Content/Upgraders/AudioClipUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "Engine/Streaming/StreamingGroup.h"
#include "Engine/Serialization/MemoryReadStream.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Tools/AudioTool/OggVorbisDecoder.h"
#include "Engine/Tools/AudioTool/AudioTool.h"
#include "Engine/Threading/Threading.h"
REGISTER_BINARY_ASSET_WITH_UPGRADER(AudioClip, "FlaxEngine.AudioClip", AudioClipUpgrader, false);
AudioClip::StreamingTask::StreamingTask(AudioClip* asset)
: _asset(asset)
, _dataLock(asset->Storage->Lock())
{
}
bool AudioClip::StreamingTask::HasReference(Object* resource) const
{
return _asset == resource;
}
bool AudioClip::StreamingTask::Run()
{
PROFILE_CPU_NAMED("AudioStreaming");
PROFILE_MEM(Audio);
AssetReference<AudioClip> clip = _asset.Get();
if (clip == nullptr || AudioBackend::Instance == nullptr)
return true;
#if TRACY_ENABLE
const StringView name(clip->GetPath());
ZoneName(*name, name.Length());
#endif
// Process the loading queue (hold the asset lock)
clip->Locker.Lock();
const auto& queue = clip->StreamingQueue;
Array<int32, FixedAllocation<ASSET_FILE_DATA_CHUNKS>> loadQueue;
for (int32 i = 0; i < queue.Count(); i++)
{
const int32 idx = queue[i];
uint32& bufferID = clip->Buffers[idx];
if (bufferID == 0)
{
// Load buffers outside the asset lock to prevent lock contention
loadQueue.Add(idx);
}
else
{
// Release unused buffer
AudioBackend::Buffer::Delete(bufferID);
bufferID = 0;
}
}
clip->Locker.Unlock();
// Load missing buffers data (from asset chunks)
for (int32 i = 0; i < loadQueue.Count(); i++)
{
if (clip->WriteBuffer(loadQueue[i]))
return true;
}
// Update the sources
for (int32 sourceIndex = 0; sourceIndex < Audio::Sources.Count(); sourceIndex++)
{
// TODO: collect refs to audio clip from sources and use faster iteration (but do it thread-safe)
const auto src = Audio::Sources[sourceIndex];
if (src->Clip == clip && src->GetState() == AudioSource::States::Playing)
{
src->RequestStreamingBuffersUpdate();
}
}
return false;
}
void AudioClip::StreamingTask::OnEnd()
{
// Unlink
if (_asset)
{
ScopeLock lock(_asset->Locker);
ASSERT(_asset->_streamingTask == this);
_asset->_streamingTask = nullptr;
_asset = nullptr;
}
_dataLock.Release();
// Base
ThreadPoolTask::OnEnd();
}
AudioClip::AudioClip(const SpawnParams& params, const AssetInfo* info)
: BinaryAsset(params, info)
, StreamableResource(StreamingGroups::Instance()->Audio())
, _totalChunks(0)
, _totalChunksSize(0)
, _streamingTask(nullptr)
{
Platform::MemoryClear(&AudioHeader, sizeof(AudioHeader));
Platform::MemoryClear(&_buffersStartTimes, sizeof(_buffersStartTimes));
}
AudioClip::~AudioClip()
{
ASSERT(_streamingTask == nullptr);
}
float AudioClip::GetBufferStartTime(int32 bufferIndex) const
{
ASSERT(IsLoaded());
return _buffersStartTimes[bufferIndex];
}
int32 AudioClip::GetFirstBufferIndex(float time, float& offset) const
{
ASSERT(IsLoaded());
time = Math::Clamp(time, 0.0f, GetLength());
for (int32 i = 0; i < _totalChunks; i++)
{
if (_buffersStartTimes[i + 1] > time)
{
offset = time - _buffersStartTimes[i];
#if BUILD_DEBUG
ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
#endif
return i;
}
}
/*
const float samples = static_cast<float>(AudioHeader.Info.SampleRate * AudioHeader.Info.NumChannels * (AudioHeader.Info.BitDepth / 8));
// TODO: use _buffersStartTimes
int32 pos = static_cast<int32>(time * samples);
for (int32 i = 0; i < _totalChunks; i++)
{
const uint32 size = _options.Chunks[i]->LocationInFile.Size;
pos -= size;
if (pos <= 0)
{
offset = (pos + size) / samples;
#if BUILD_DEBUG
ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f);
#endif
return i;
}
}
*/
offset = 0.0f;
return 0;
}
bool AudioClip::ExtractData(Array<byte>& resultData, AudioDataInfo& resultDataInfo)
{
ASSERT(!IsVirtual());
if (WaitForLoaded())
return true;
ScopeLock lock(Locker);
auto storageLock = Storage->LockSafe();
// Allocate memory
ASSERT(_totalChunksSize > 0);
resultData.Resize(_totalChunksSize);
// Load and copy data
if (LoadChunks(ALL_ASSET_CHUNKS))
return true;
int32 pos = 0;
Storage->LockChunks();
for (int32 i = 0; i < _totalChunks; i++)
{
const auto chunk = GetChunk(i);
const uint32 size = chunk->Size();
Platform::MemoryCopy(resultData.Get() + pos, chunk->Get(), size);
pos += size;
}
Storage->UnlockChunks();
ASSERT(pos == _totalChunksSize);
// Copy header
resultDataInfo = AudioHeader.Info;
return false;
}
bool AudioClip::ExtractDataFloat(Array<float>& resultData, AudioDataInfo& resultDataInfo)
{
// Extract PCM data
Array<byte> data;
if (ExtractDataRaw(data, resultDataInfo))
return true;
// Convert to float
resultData.Resize(resultDataInfo.NumSamples);
AudioTool::ConvertToFloat(data.Get(), resultDataInfo.BitDepth, resultData.Get(), resultDataInfo.NumSamples);
resultDataInfo.BitDepth = 32;
return false;
}
bool AudioClip::ExtractDataRaw(Array<byte>& resultData, AudioDataInfo& resultDataInfo)
{
if (WaitForLoaded())
return true;
ScopeLock lock(Locker);
switch (Format())
{
case AudioFormat::Raw:
{
return ExtractData(resultData, resultDataInfo);
}
case AudioFormat::Vorbis:
{
#if COMPILE_WITH_OGG_VORBIS
Array<byte> vorbisData;
AudioDataInfo vorbisDataInfo;
if (ExtractData(vorbisData, vorbisDataInfo))
return true;
OggVorbisDecoder decoder;
MemoryReadStream stream(vorbisData.Get(), vorbisData.Count());
return decoder.Convert(&stream, resultDataInfo, resultData);
#else
LOG(Warning, "OggVorbisDecoder is disabled.");
return true;
#endif
}
}
return true;
}
void AudioClip::CancelStreaming()
{
Asset::CancelStreaming();
CancelStreamingTasks();
}
int32 AudioClip::GetMaxResidency() const
{
return _totalChunks;
}
int32 AudioClip::GetCurrentResidency() const
{
return Buffers.Count();
}
int32 AudioClip::GetAllocatedResidency() const
{
return Buffers.Count();
}
bool AudioClip::CanBeUpdated() const
{
// Check if is ready and has no streaming tasks running
return _totalChunks != 0 && _streamingTask == nullptr;
}
Task* AudioClip::UpdateAllocation(int32 residency)
{
// Audio clips are not using dynamic allocation feature
return nullptr;
}
Task* AudioClip::CreateStreamingTask(int32 residency)
{
ScopeLock lock(Locker);
ASSERT(_totalChunks != 0 && Math::IsInRange(residency, 0, _totalChunks) && _streamingTask == nullptr);
Task* result = nullptr;
// Requests assets chunks data
for (int32 i = 0; i < StreamingQueue.Count(); i++)
{
const int32 idx = StreamingQueue[i];
if (Buffers[idx] == 0)
{
const auto task = (Task*)RequestChunkDataAsync(idx);
if (task)
{
if (result)
result->ContinueWith(task);
else
result = task;
}
}
}
// Add streaming task
_streamingTask = New<StreamingTask>(this);
if (result)
result->ContinueWith(_streamingTask);
else
result = _streamingTask;
return result;
}
void AudioClip::CancelStreamingTasks()
{
ScopeLock lock(Locker);
if (_streamingTask)
{
_streamingTask->Cancel();
ASSERT_LOW_LAYER(_streamingTask == nullptr);
}
}
bool AudioClip::init(AssetInitData& initData)
{
// Validate
if (initData.CustomData.Length() != sizeof(AudioHeader))
{
LOG(Warning, "Missing audio data.");
return true;
}
// Load header
Platform::MemoryCopy(&AudioHeader, initData.CustomData.Get(), sizeof(AudioHeader));
return false;
}
Asset::LoadResult AudioClip::load()
{
PROFILE_MEM(Audio);
#if !COMPILE_WITH_OGG_VORBIS
if (AudioHeader.Format == AudioFormat::Vorbis)
{
LOG(Warning, "OggVorbisDecoder is disabled.");
return LoadResult::Failed;
}
#endif
// Count chunks with an audio data
_totalChunks = 0;
while (_totalChunks < ASSET_FILE_DATA_CHUNKS && HasChunk(_totalChunks))
_totalChunks++;
// Fill buffers with empty ids
for (int32 i = 0; i < _totalChunks; i++)
Buffers.Add(0);
// Setup the buffers start time (used by the streaming)
const float scale = 1.0f / static_cast<float>(Math::Max(1U, AudioHeader.Info.SampleRate * AudioHeader.Info.NumChannels));
_totalChunksSize = AudioHeader.ImportedSize;
_buffersStartTimes[0] = 0.0f;
for (int32 i = 0; i < _totalChunks; i++)
{
_buffersStartTimes[i + 1] = _buffersStartTimes[i] + AudioHeader.SamplesPerChunk[i] * scale;
}
#if !BUILD_RELEASE
// Validate buffer start times
if (!Math::NearEqual(_buffersStartTimes[_totalChunks], GetLength(), 1.0f / 60.0f))
{
LOG(Warning, "Invalid audio buffers data size. Expected length: {0}s", GetLength());
for (int32 i = 0; i < _totalChunks + 1; i++)
LOG(Warning, "StartTime[{0}] = {1}s", i, _buffersStartTimes[i]);
return LoadResult::InvalidData;
}
#endif
// Check if use audio streaming
if (AudioHeader.Streamable)
{
// Do nothing because data streaming starts when any AudioSource requests the data
StartStreaming(false);
return LoadResult::Ok;
}
// Load the whole audio at once
if (LoadChunk(0))
return LoadResult::CannotLoadData;
// Create single buffer
Buffers[0] = AudioBackend::Buffer::Create();
// Write data to audio buffer
if (WriteBuffer(0))
return LoadResult::Failed;
return LoadResult::Ok;
}
void AudioClip::unload(bool isReloading)
{
bool hasAnyBuffer = false;
for (const uint32 bufferID : Buffers)
hasAnyBuffer |= bufferID != 0;
// Stop any audio sources that are using this clip right now
// TODO: find better way to collect audio sources using audio clip and impl it for AudioStreamingHandler too
for (int32 sourceIndex = 0; sourceIndex < Audio::Sources.Count(); sourceIndex++)
{
const auto src = Audio::Sources[sourceIndex];
if (src->Clip == this)
src->Stop();
}
StopStreaming();
CancelStreamingTasks();
StreamingQueue.Clear();
if (hasAnyBuffer && AudioBackend::Instance)
{
for (uint32 bufferID : Buffers)
{
if (bufferID != 0)
AudioBackend::Buffer::Delete(bufferID);
}
}
Buffers.Clear();
_totalChunks = 0;
Platform::MemoryClear(&AudioHeader, sizeof(AudioHeader));
}
bool AudioClip::WriteBuffer(int32 chunkIndex)
{
// Ensure audio backend exists
if (AudioBackend::Instance == nullptr)
return true;
PROFILE_CPU();
PROFILE_MEM(Audio);
const auto chunk = GetChunk(chunkIndex);
if (chunk == nullptr || chunk->IsMissing())
{
LOG(Warning, "Missing audio data.");
return true;
}
Span<byte> data;
Array<byte> tmp1, tmp2;
AudioDataInfo info = AudioHeader.Info;
const uint32 bytesPerSample = info.BitDepth / 8;
ZoneValue(chunk->Size() / 1024); // Audio data size (in kB)
// Get raw data or decompress it
switch (Format())
{
case AudioFormat::Vorbis:
{
#if COMPILE_WITH_OGG_VORBIS
PROFILE_CPU_NAMED("OggVorbisDecode");
OggVorbisDecoder decoder;
MemoryReadStream stream(chunk->Get(), chunk->Size());
AudioDataInfo tmpInfo;
if (decoder.Convert(&stream, tmpInfo, tmp1))
{
LOG(Warning, "Audio data decode failed (OggVorbisDecoder).");
return true;
}
// TODO: validate decompressed data header info?
data = Span<byte>(tmp1.Get(), tmp1.Count());
#else
LOG(Warning, "OggVorbisDecoder is disabled.");
return true;
#endif
}
break;
case AudioFormat::Raw:
data = Span<byte>(chunk->Get(), chunk->Size());
break;
default:
return true;
}
info.NumSamples = Math::AlignDown(data.Length() / bytesPerSample, info.NumChannels * bytesPerSample);
// Convert to Mono if used as 3D source and backend doesn't support it
if (Is3D() && info.NumChannels > 1 && EnumHasNoneFlags(AudioBackend::Features(), AudioBackend::FeatureFlags::SpatialMultiChannel))
{
const uint32 samplesPerChannel = info.NumSamples / info.NumChannels;
const uint32 monoBufferSize = samplesPerChannel * bytesPerSample;
tmp2.Resize(monoBufferSize);
AudioTool::ConvertToMono(data.Get(), tmp2.Get(), info.BitDepth, samplesPerChannel, info.NumChannels);
info.NumChannels = 1;
info.NumSamples = samplesPerChannel;
data = Span<byte>(tmp2.Get(), tmp2.Count());
}
// Write samples to the audio buffer (create one if missing)
Locker.Lock(); // StreamingTask loads buffers without lock so do it here
uint32& bufferID = Buffers[chunkIndex];
if (bufferID == 0)
bufferID = AudioBackend::Buffer::Create();
AudioBackend::Buffer::Write(bufferID, data.Get(), info);
Locker.Unlock();
return false;
}