837 lines
26 KiB
C++
837 lines
26 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AudioMixerSourceBuffer.h"
|
|
#include "AudioMixerSourceDecode.h"
|
|
#include "ContentStreaming.h"
|
|
#include "AudioDecompress.h"
|
|
#include "Misc/ScopeTryLock.h"
|
|
#include "DSP/FloatArrayMath.h"
|
|
|
|
namespace Audio
|
|
{
|
|
int32 DirectProceduralRenderingCVar = 0;
|
|
FAutoConsoleVariableRef CVarDirectProceduralRendering(
|
|
TEXT("au.DirectProceduralRendering"),
|
|
DirectProceduralRenderingCVar,
|
|
TEXT("Render procedural sources (e.g. MetaSounds) on demand on the render thread, instead of using background tasks.\n")
|
|
TEXT("It is only safe to change this setting when there are no sounds playing.\n")
|
|
TEXT("0: Disabled, 1: Enabled"),
|
|
ECVF_Default);
|
|
|
|
bool FRawPCMDataBuffer::GetNextBuffer(TArrayView<float> OutSourceBufferPtr, const uint32 NumSampleToGet)
|
|
{
|
|
// TODO: support loop counts
|
|
float* OutBufferPtr = OutSourceBufferPtr.GetData();
|
|
int16* DataPtr = (int16*)Data;
|
|
|
|
if (LoopCount == Audio::LOOP_FOREVER)
|
|
{
|
|
bool bIsFinishedOrLooped = false;
|
|
for (uint32 Sample = 0; Sample < NumSampleToGet; ++Sample)
|
|
{
|
|
OutBufferPtr[Sample] = DataPtr[CurrentSample++] / 32768.0f;
|
|
|
|
// Loop around if we're looping
|
|
if (CurrentSample >= NumSamples)
|
|
{
|
|
CurrentSample = 0;
|
|
bIsFinishedOrLooped = true;
|
|
}
|
|
}
|
|
return bIsFinishedOrLooped;
|
|
}
|
|
else if (CurrentSample < NumSamples)
|
|
{
|
|
uint32 Sample = 0;
|
|
while (Sample < NumSampleToGet && CurrentSample < NumSamples)
|
|
{
|
|
OutBufferPtr[Sample++] = (float)DataPtr[CurrentSample++] / 32768.0f;
|
|
}
|
|
|
|
// Zero out the rest of the buffer
|
|
FMemory::Memzero(&OutBufferPtr[Sample], (NumSampleToGet - Sample) * sizeof(float));
|
|
}
|
|
else
|
|
{
|
|
FMemory::Memzero(OutBufferPtr, NumSampleToGet * sizeof(float));
|
|
}
|
|
|
|
// If the current sample is greater or equal to num samples we hit the end of the buffer
|
|
return CurrentSample >= NumSamples;
|
|
}
|
|
|
|
TSharedPtr<FMixerSourceBuffer, ESPMode::ThreadSafe> FMixerSourceBuffer::Create(FMixerSourceBufferInitArgs& InArgs, TArray<FAudioParameter>&& InDefaultParams)
|
|
{
|
|
LLM_SCOPE(ELLMTag::AudioMixer);
|
|
|
|
// Fail if the Wave has been flagged to contain an error
|
|
if (InArgs.SoundWave && InArgs.SoundWave->HasError())
|
|
{
|
|
UE_LOG(LogAudioMixer, VeryVerbose, TEXT("FMixerSourceBuffer::Create failed as '%s' is flagged as containing errors"), *InArgs.SoundWave->GetName());
|
|
return {};
|
|
}
|
|
|
|
TSharedPtr<FMixerSourceBuffer, ESPMode::ThreadSafe> NewSourceBuffer = MakeShareable(new FMixerSourceBuffer(InArgs, MoveTemp(InDefaultParams)));
|
|
|
|
return NewSourceBuffer;
|
|
}
|
|
|
|
FMixerSourceBuffer::FMixerSourceBuffer(FMixerSourceBufferInitArgs& InArgs, TArray<FAudioParameter>&& InDefaultParams)
|
|
: NumBuffersQeueued(0)
|
|
, CurrentBuffer(0)
|
|
, SoundWave(InArgs.SoundWave)
|
|
, AsyncRealtimeAudioTask(nullptr)
|
|
, DecompressionState(nullptr)
|
|
, LoopingMode(InArgs.LoopingMode)
|
|
, NumChannels(InArgs.Buffer->NumChannels)
|
|
, BufferType(InArgs.Buffer->GetType())
|
|
, NumPrecacheFrames(InArgs.SoundWave->NumPrecacheFrames)
|
|
, AuioDeviceID(InArgs.AudioDeviceID)
|
|
, InstanceID(InArgs.InstanceID)
|
|
, WaveName(InArgs.SoundWave->GetFName())
|
|
#if ENABLE_AUDIO_DEBUG
|
|
, SampleRate(InArgs.SampleRate)
|
|
#endif // ENABLE_AUDIO_DEBUG
|
|
, bInitialized(false)
|
|
, bBufferFinished(false)
|
|
, bPlayedCachedBuffer(false)
|
|
, bIsSeeking(InArgs.bIsSeeking)
|
|
, bLoopCallback(false)
|
|
, bProcedural(InArgs.SoundWave->bProcedural)
|
|
, bIsBus(InArgs.SoundWave->bIsSourceBus)
|
|
, bForceSyncDecode(InArgs.bForceSyncDecode)
|
|
, bHasError(false)
|
|
, bDirectRendering(InArgs.SoundWave->bProcedural && DirectProceduralRenderingCVar)
|
|
{
|
|
// TODO: remove the need to do this here. 1) remove need for decoders to depend on USoundWave and 2) remove need for procedural sounds to use USoundWaveProcedural
|
|
InArgs.SoundWave->AddPlayingSource(this);
|
|
|
|
// Retrieve a sound generator if this is a procedural sound wave
|
|
if (bProcedural)
|
|
{
|
|
FSoundGeneratorInitParams InitParams;
|
|
InitParams.AudioDeviceID = InArgs.AudioDeviceID;
|
|
InitParams.AudioComponentId = InArgs.AudioComponentID;
|
|
InitParams.SampleRate = InArgs.SampleRate;
|
|
InitParams.AudioMixerNumOutputFrames = InArgs.AudioMixerNumOutputFrames;
|
|
InitParams.NumChannels = NumChannels;
|
|
InitParams.NumFramesPerCallback = MONO_PCM_BUFFER_SAMPLES;
|
|
InitParams.InstanceID = InArgs.InstanceID;
|
|
InitParams.bIsPreviewSound = InArgs.bIsPreviewSound;
|
|
InitParams.StartTime = InArgs.StartTime;
|
|
|
|
SoundGenerator = InArgs.SoundWave->CreateSoundGenerator(InitParams, MoveTemp(InDefaultParams));
|
|
|
|
// In the case of procedural audio generation, the mixer source buffer will never "loop" -- i.e. when it's done, it's done
|
|
LoopingMode = LOOP_Never;
|
|
}
|
|
|
|
// Only allocate one buffer to render into when doing direct rendering
|
|
const int32 NumBuffers = bDirectRendering ? 1 : Audio::MAX_BUFFERS_QUEUED;
|
|
|
|
const uint32 TotalSamples = (bDirectRendering && SoundGenerator.IsValid()) ? SoundGenerator->GetDesiredNumSamplesToRenderPerCallback() : MONO_PCM_BUFFER_SAMPLES * NumChannels;
|
|
for (int32 BufferIndex = 0; BufferIndex < NumBuffers; ++BufferIndex)
|
|
{
|
|
SourceVoiceBuffers.Add(MakeShared<FAlignedFloatBuffer, ESPMode::ThreadSafe>());
|
|
|
|
// Prepare the memory to fit the max number of samples
|
|
SourceVoiceBuffers[BufferIndex]->Reset(TotalSamples);
|
|
}
|
|
}
|
|
|
|
FMixerSourceBuffer::~FMixerSourceBuffer()
|
|
{
|
|
// GC methods may get called from the game thread during the destructor
|
|
// These methods will trylock and early exit if we have this lock
|
|
FScopeLock Lock(&SoundWaveCritSec);
|
|
|
|
// OnEndGenerate calls EnsureTaskFinishes,
|
|
// which will make sure we have completed our async realtime task before deleting the decompression state
|
|
OnEndGenerate();
|
|
|
|
// Clean up decompression state after things have been finished using it
|
|
DeleteDecoder();
|
|
|
|
if (SoundWave)
|
|
{
|
|
SoundWave->RemovePlayingSource(this);
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::SetDecoder(ICompressedAudioInfo* InCompressedAudioInfo)
|
|
{
|
|
if (DecompressionState == nullptr)
|
|
{
|
|
DecompressionState = InCompressedAudioInfo;
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::SetPCMData(const FRawPCMDataBuffer& InPCMDataBuffer)
|
|
{
|
|
check(BufferType == EBufferType::PCM || BufferType == EBufferType::PCMPreview);
|
|
RawPCMDataBuffer = InPCMDataBuffer;
|
|
}
|
|
|
|
void FMixerSourceBuffer::SetCachedRealtimeFirstBuffers(TArray<uint8>&& InPrecachedBuffers)
|
|
{
|
|
CachedRealtimeFirstBuffer = MoveTemp(InPrecachedBuffers);
|
|
}
|
|
|
|
bool FMixerSourceBuffer::Init()
|
|
{
|
|
// We have successfully initialized which means our SoundWave has been flagged as bIsActive
|
|
// GC can run between PreInit and Init so when cleaning up FMixerSourceBuffer, we don't want to touch SoundWave unless bInitailized is true.
|
|
// SoundWave->bIsSoundActive will prevent GC until it is released in audio render thread
|
|
bInitialized = true;
|
|
|
|
switch (BufferType)
|
|
{
|
|
case EBufferType::PCM:
|
|
case EBufferType::PCMPreview:
|
|
SubmitInitialPCMBuffers();
|
|
break;
|
|
|
|
case EBufferType::PCMRealTime:
|
|
case EBufferType::Streaming:
|
|
if (!bDirectRendering)
|
|
{
|
|
SubmitInitialRealtimeBuffers();
|
|
}
|
|
break;
|
|
|
|
case EBufferType::Invalid:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FMixerSourceBuffer::OnBufferEnd()
|
|
{
|
|
if (bDirectRendering)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
|
|
// If the buffer is flagged as complete and there's nothing queued remaining.
|
|
const bool bBufferCompleted = (NumBuffersQeueued == 0 && bBufferFinished);
|
|
|
|
// If we're procedural we must have a procedural SoundWave pointer to continue.
|
|
const bool bProceduralStateBad = (bProcedural && !SoundWave);
|
|
|
|
// If we're non-procedural and we don't have a decoder, bail. This can happen when the wave is GC'd.
|
|
// The Decoder and SoundWave is deleted on the GameThread via FMixerSourceBuffer::OnBeginDestroy
|
|
// Although this is bad state it's not an error, so just bail here.
|
|
const bool bDecompressionStateBad = (!bProcedural && DecompressionState == nullptr);
|
|
|
|
if (!Lock.IsLocked() || bBufferCompleted || bProceduralStateBad || bDecompressionStateBad || bHasError)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ProcessRealTimeSource();
|
|
}
|
|
|
|
int32 FMixerSourceBuffer::GetNumBuffersQueued() const
|
|
{
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
if (Lock.IsLocked())
|
|
{
|
|
return NumBuffersQeueued;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
TSharedPtr<FAlignedFloatBuffer, ESPMode::ThreadSafe> FMixerSourceBuffer::GetNextBuffer()
|
|
{
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
if (!Lock.IsLocked())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
if (bDirectRendering)
|
|
{
|
|
// Do rendering immediately.
|
|
FProceduralAudioTaskData NewTaskData;
|
|
|
|
// Make sure we actually have something to render
|
|
check(SoundGenerator.IsValid() || (SoundWave && SoundWave->bProcedural));
|
|
|
|
const int32 MaxSamples = SoundGenerator.IsValid() ? SoundGenerator->GetDesiredNumSamplesToRenderPerCallback() : MONO_PCM_BUFFER_SAMPLES * NumChannels;
|
|
check(CurrentBuffer == 0);
|
|
SourceVoiceBuffers[0]->SetNumUninitialized(MaxSamples, EAllowShrinking::No);
|
|
|
|
NewTaskData.SourceBuffer = this;
|
|
NewTaskData.SoundGenerator = SoundGenerator;
|
|
NewTaskData.AudioData = SourceVoiceBuffers[0]->GetData();
|
|
NewTaskData.NumSamples = MaxSamples;
|
|
|
|
FProceduralAudioTaskResults Results;
|
|
DoProceduralRendering(NewTaskData, Results);
|
|
FinishProceduralRendering(Results);
|
|
bBufferFinished = Results.bIsFinished;
|
|
|
|
return SourceVoiceBuffers[0];
|
|
}
|
|
|
|
TSharedPtr<FAlignedFloatBuffer, ESPMode::ThreadSafe> NewBufferPtr;
|
|
BufferQueue.Dequeue(NewBufferPtr);
|
|
--NumBuffersQeueued;
|
|
return NewBufferPtr;
|
|
}
|
|
|
|
void FMixerSourceBuffer::DoProceduralRendering(const FProceduralAudioTaskData& ProceduralTaskData, FProceduralAudioTaskResults& ProceduralResult)
|
|
{
|
|
#if ENABLE_AUDIO_DEBUG
|
|
FScopeDecodeTimer Timer(&ProceduralResult.CPUDuration);
|
|
#endif // if ENABLE_AUDIO_DEBUG
|
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FAsyncDecodeWorker_Procedural);
|
|
if (ProceduralTaskData.SoundGenerator.IsValid())
|
|
{
|
|
// Generators are responsible to zero memory in case they can't generate the requested amount of samples
|
|
ProceduralResult.NumSamplesWritten = ProceduralTaskData.SoundGenerator->GetNextBuffer(ProceduralTaskData.AudioData, ProceduralTaskData.NumSamples);
|
|
ProceduralResult.bIsFinished = ProceduralTaskData.SoundGenerator->IsFinished();
|
|
ProceduralResult.RelativeRenderCost = ProceduralTaskData.SoundGenerator->GetRelativeRenderCost();
|
|
}
|
|
else
|
|
{
|
|
// Make sure we've been flagged as active
|
|
if (!SoundWave || !SoundWave->IsGeneratingAudio())
|
|
{
|
|
// Act as if we generated audio, but return silence.
|
|
FMemory::Memzero(ProceduralTaskData.AudioData, ProceduralTaskData.NumSamples * sizeof(float));
|
|
ProceduralResult.NumSamplesWritten = ProceduralTaskData.NumSamples;
|
|
return;
|
|
}
|
|
|
|
// If we're not a float format, we need to convert the format to float
|
|
const EAudioMixerStreamDataFormat::Type FormatType = SoundWave->GetGeneratedPCMDataFormat();
|
|
if (FormatType != EAudioMixerStreamDataFormat::Float)
|
|
{
|
|
check(FormatType == EAudioMixerStreamDataFormat::Int16);
|
|
|
|
int32 ByteSize = NumChannels * ProceduralTaskData.NumSamples * sizeof(int16);
|
|
|
|
TArray<uint8> DecodeBuffer;
|
|
DecodeBuffer.AddUninitialized(ByteSize);
|
|
|
|
const int32 NumBytesWritten = SoundWave->GeneratePCMData(DecodeBuffer.GetData(), ProceduralTaskData.NumSamples);
|
|
|
|
check(NumBytesWritten <= ByteSize);
|
|
|
|
ProceduralResult.NumSamplesWritten = NumBytesWritten / sizeof(int16);
|
|
Audio::ArrayPcm16ToFloat(MakeArrayView((int16*)DecodeBuffer.GetData(), ProceduralResult.NumSamplesWritten)
|
|
, MakeArrayView(ProceduralTaskData.AudioData, ProceduralResult.NumSamplesWritten));
|
|
}
|
|
else
|
|
{
|
|
const int32 NumBytesWritten = SoundWave->GeneratePCMData((uint8*)ProceduralTaskData.AudioData, ProceduralTaskData.NumSamples);
|
|
ProceduralResult.NumSamplesWritten = NumBytesWritten / sizeof(float);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::FinishProceduralRendering(const FProceduralAudioTaskResults& TaskResult)
|
|
{
|
|
// When doing direct rendering, should only ever use buffer 0
|
|
check(!bDirectRendering || CurrentBuffer == 0);
|
|
|
|
SourceVoiceBuffers[CurrentBuffer]->SetNum(TaskResult.NumSamplesWritten, EAllowShrinking::No);
|
|
|
|
#if ENABLE_AUDIO_DEBUG
|
|
double AudioDuration = static_cast<double>(TaskResult.NumSamplesWritten) / static_cast<double>(FMath::Max(1, NumChannels * SampleRate));
|
|
UpdateCPUCoreUtilization(TaskResult.CPUDuration, AudioDuration);
|
|
#endif // ENABLE_AUDIO_DEBUG
|
|
|
|
// Set the render cost encountered during the last render
|
|
SetRelativeRenderCost(TaskResult.RelativeRenderCost);
|
|
}
|
|
|
|
void FMixerSourceBuffer::SubmitInitialPCMBuffers()
|
|
{
|
|
CurrentBuffer = 0;
|
|
|
|
RawPCMDataBuffer.NumSamples = RawPCMDataBuffer.DataSize / sizeof(int16);
|
|
RawPCMDataBuffer.CurrentSample = 0;
|
|
|
|
// Only submit data if we've successfully loaded it
|
|
if (!RawPCMDataBuffer.Data || !RawPCMDataBuffer.DataSize)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RawPCMDataBuffer.LoopCount = (LoopingMode != LOOP_Never) ? Audio::LOOP_FOREVER : 0;
|
|
|
|
// Submit the first two format-converted chunks to the source voice
|
|
const uint32 NumSamplesPerBuffer = MONO_PCM_BUFFER_SAMPLES * NumChannels;
|
|
int16* RawPCMBufferDataPtr = (int16*)RawPCMDataBuffer.Data;
|
|
|
|
// Prepare the buffer for the PCM submission
|
|
SourceVoiceBuffers[0]->Reset(NumSamplesPerBuffer);
|
|
SourceVoiceBuffers[0]->AddZeroed(NumSamplesPerBuffer);
|
|
|
|
RawPCMDataBuffer.GetNextBuffer(*SourceVoiceBuffers[0], NumSamplesPerBuffer);
|
|
|
|
SubmitBuffer(SourceVoiceBuffers[0]);
|
|
|
|
CurrentBuffer = 1;
|
|
}
|
|
|
|
void FMixerSourceBuffer::SubmitInitialRealtimeBuffers()
|
|
{
|
|
static_assert(PLATFORM_NUM_AUDIODECOMPRESSION_PRECACHE_BUFFERS <= 2 && PLATFORM_NUM_AUDIODECOMPRESSION_PRECACHE_BUFFERS >= 0, "Unsupported number of precache buffers.");
|
|
check(!bDirectRendering);
|
|
|
|
CurrentBuffer = 0;
|
|
|
|
bPlayedCachedBuffer = false;
|
|
if (!bIsSeeking && CachedRealtimeFirstBuffer.Num() > 0)
|
|
{
|
|
bPlayedCachedBuffer = true;
|
|
|
|
const uint32 NumSamples = NumPrecacheFrames * NumChannels;
|
|
const uint32 BufferSize = NumSamples * sizeof(int16);
|
|
|
|
// Format convert the first cached buffers
|
|
#if (PLATFORM_NUM_AUDIODECOMPRESSION_PRECACHE_BUFFERS == 2)
|
|
{
|
|
// Prepare the precache buffer memory
|
|
for (int32 i = 0; i < 2; ++i)
|
|
{
|
|
SourceVoiceBuffers[i]->Reset();
|
|
SourceVoiceBuffers[i]->AddZeroed(NumSamples);
|
|
}
|
|
|
|
int16* CachedBufferPtr0 = (int16*)CachedRealtimeFirstBuffer.GetData();
|
|
int16* CachedBufferPtr1 = (int16*)(CachedRealtimeFirstBuffer.GetData() + BufferSize);
|
|
float* AudioData0 = SourceVoiceBuffers[0]->GetData();
|
|
float* AudioData1 = SourceVoiceBuffers[1]->GetData();
|
|
|
|
Audio::ArrayPcm16ToFloat(MakeArrayView(CachedBufferPtr0, NumSamples), MakeArrayView(AudioData0, NumSamples));
|
|
Audio::ArrayPcm16ToFloat(MakeArrayView(CachedBufferPtr1, NumSamples), MakeArrayView(AudioData1, NumSamples));
|
|
|
|
// Submit the already decoded and cached audio buffers
|
|
SubmitBuffer(SourceVoiceBuffers[0]);
|
|
SubmitBuffer(SourceVoiceBuffers[1]);
|
|
|
|
CurrentBuffer = 2;
|
|
}
|
|
#elif (PLATFORM_NUM_AUDIODECOMPRESSION_PRECACHE_BUFFERS == 1)
|
|
{
|
|
SourceVoiceBuffers[0]->AudioData.Reset();
|
|
SourceVoiceBuffers[0]->AudioData.AddZeroed(NumSamples);
|
|
|
|
int16* CachedBufferPtr0 = (int16*)CachedRealtimeFirstBuffer.GetData();
|
|
float* AudioData0 = SourceVoiceBuffers[0]->AudioData.GetData();
|
|
Audio::ArrayPcm16ToFloat(MakeArrayView(CachedBufferPtr0, NumSamples), MakeArrayView(AudioData0, NumSamples));
|
|
|
|
// Submit the already decoded and cached audio buffers
|
|
SubmitBuffer(SourceVoiceBuffers[0]);
|
|
|
|
CurrentBuffer = 1;
|
|
}
|
|
#endif
|
|
}
|
|
else if (!bIsBus)
|
|
{
|
|
ProcessRealTimeSource();
|
|
}
|
|
}
|
|
|
|
bool FMixerSourceBuffer::ReadMoreRealtimeData(ICompressedAudioInfo* InDecoder, const int32 BufferIndex, EBufferReadMode BufferReadMode)
|
|
{
|
|
check(!bDirectRendering);
|
|
const int32 MaxSamples = MONO_PCM_BUFFER_SAMPLES * NumChannels;
|
|
|
|
SourceVoiceBuffers[BufferIndex]->Reset();
|
|
SourceVoiceBuffers[BufferIndex]->AddUninitialized(MaxSamples);
|
|
|
|
if (bProcedural)
|
|
{
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
|
|
if (Lock.IsLocked() && SoundWave)
|
|
{
|
|
FProceduralAudioTaskData NewTaskData;
|
|
|
|
// Make sure we actually have something to render
|
|
check(SoundGenerator.IsValid() || (SoundWave && SoundWave->bProcedural));
|
|
|
|
NewTaskData.SourceBuffer = this;
|
|
NewTaskData.SoundGenerator = SoundGenerator;
|
|
NewTaskData.AudioData = SourceVoiceBuffers[BufferIndex]->GetData();
|
|
NewTaskData.NumSamples = MaxSamples;
|
|
AsyncTaskStartTimeInCycles = FPlatformTime::Cycles64();
|
|
check(!AsyncRealtimeAudioTask);
|
|
AsyncRealtimeAudioTask = CreateAudioTask(AuioDeviceID, NewTaskData);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
else if (BufferType != EBufferType::PCMRealTime && BufferType != EBufferType::Streaming)
|
|
{
|
|
check(RawPCMDataBuffer.Data != nullptr);
|
|
|
|
// Read the next raw PCM buffer into the source buffer index. This converts raw PCM to float.
|
|
return RawPCMDataBuffer.GetNextBuffer(*SourceVoiceBuffers[BufferIndex], MaxSamples);
|
|
}
|
|
|
|
// Handle the case that the decoder has an error and can't continue.
|
|
if (InDecoder && InDecoder->HasError())
|
|
{
|
|
FMemory::Memzero(SourceVoiceBuffers[BufferIndex]->GetData(), MaxSamples * sizeof(float));
|
|
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
if (Lock.IsLocked() && SoundWave)
|
|
{
|
|
SoundWave->SetError(TEXT("ICompressedAudioInfo::HasError() flagged on the Decoder"));
|
|
}
|
|
|
|
bHasError = true;
|
|
bBufferFinished = true;
|
|
return false;
|
|
}
|
|
|
|
check(InDecoder != nullptr);
|
|
|
|
FDecodeAudioTaskData NewTaskData;
|
|
NewTaskData.AudioData = SourceVoiceBuffers[BufferIndex]->GetData();
|
|
NewTaskData.DecompressionState = InDecoder;
|
|
NewTaskData.BufferType = BufferType;
|
|
NewTaskData.NumChannels = NumChannels;
|
|
NewTaskData.bLoopingMode = LoopingMode != LOOP_Never;
|
|
NewTaskData.bSkipFirstBuffer = (BufferReadMode == EBufferReadMode::AsynchronousSkipFirstFrame);
|
|
NewTaskData.NumFramesToDecode = MONO_PCM_BUFFER_SAMPLES;
|
|
NewTaskData.NumPrecacheFrames = NumPrecacheFrames;
|
|
NewTaskData.bForceSyncDecode = bForceSyncDecode;
|
|
|
|
AsyncTaskStartTimeInCycles = FPlatformTime::Cycles64();
|
|
FScopeLock Lock(&DecodeTaskCritSec);
|
|
check(!AsyncRealtimeAudioTask);
|
|
AsyncRealtimeAudioTask = CreateAudioTask(AuioDeviceID, NewTaskData);
|
|
|
|
return false;
|
|
}
|
|
|
|
void FMixerSourceBuffer::SubmitRealTimeSourceData(const bool bInIsFinishedOrLooped)
|
|
{
|
|
// Have we reached the end of the sound
|
|
if (bInIsFinishedOrLooped)
|
|
{
|
|
switch (LoopingMode)
|
|
{
|
|
case LOOP_Never:
|
|
// Play out any queued buffers - once there are no buffers left, the state check at the beginning of IsFinished will fire
|
|
bBufferFinished = true;
|
|
break;
|
|
|
|
case LOOP_WithNotification:
|
|
// If we have just looped, and we are looping, send notification
|
|
// This will trigger a WaveInstance->NotifyFinished() in the FXAudio2SoundSournce::IsFinished() function on main thread.
|
|
bLoopCallback = true;
|
|
break;
|
|
|
|
case LOOP_Forever:
|
|
// Let the sound loop indefinitely
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (SourceVoiceBuffers[CurrentBuffer]->Num() > 0)
|
|
{
|
|
SubmitBuffer(SourceVoiceBuffers[CurrentBuffer]);
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::ProcessRealTimeSource()
|
|
{
|
|
FScopeLock Lock(&DecodeTaskCritSec);
|
|
|
|
// Finish current decoding task
|
|
if (AsyncRealtimeAudioTask)
|
|
{
|
|
AsyncRealtimeAudioTask->EnsureCompletion();
|
|
|
|
bool bIsFinishedOrLooped = false;
|
|
|
|
switch (AsyncRealtimeAudioTask->GetType())
|
|
{
|
|
case EAudioTaskType::Decode:
|
|
{
|
|
FDecodeAudioTaskResults TaskResult;
|
|
AsyncRealtimeAudioTask->GetResult(TaskResult);
|
|
bIsFinishedOrLooped = TaskResult.bIsFinishedOrLooped;
|
|
|
|
SourceVoiceBuffers[CurrentBuffer]->SetNum(TaskResult.NumSamplesWritten, EAllowShrinking::No);
|
|
#if ENABLE_AUDIO_DEBUG
|
|
double AudioDuration = static_cast<double>(TaskResult.NumSamplesWritten) / static_cast<double>(FMath::Max(1, NumChannels * SampleRate));
|
|
UpdateCPUCoreUtilization(TaskResult.CPUDuration, AudioDuration);
|
|
#endif // ENABLE_AUDIO_DEBUG
|
|
}
|
|
break;
|
|
|
|
case EAudioTaskType::Procedural:
|
|
{
|
|
FProceduralAudioTaskResults TaskResult;
|
|
AsyncRealtimeAudioTask->GetResult(TaskResult);
|
|
FinishProceduralRendering(TaskResult);
|
|
bIsFinishedOrLooped = TaskResult.bIsFinished;
|
|
}
|
|
break;
|
|
}
|
|
|
|
delete AsyncRealtimeAudioTask;
|
|
AsyncRealtimeAudioTask = nullptr;
|
|
AsyncTaskStartTimeInCycles = 0;
|
|
|
|
SubmitRealTimeSourceData(bIsFinishedOrLooped);
|
|
}
|
|
|
|
if (FAudioDeviceManager* ADM = FAudioDeviceManager::Get())
|
|
{
|
|
if (FAudioDevice* AudioDevice = ADM->GetAudioDeviceRaw(AuioDeviceID))
|
|
{
|
|
UAudioBusSubsystem* AudioBusSubsystem = AudioDevice->GetSubsystem<UAudioBusSubsystem>();
|
|
check(AudioBusSubsystem);
|
|
AudioBusSubsystem->ConnectPatches(InstanceID);
|
|
}
|
|
}
|
|
|
|
// Start a new decoding task
|
|
if (!AsyncRealtimeAudioTask && !bDirectRendering)
|
|
{
|
|
// Update the buffer index
|
|
if (++CurrentBuffer > 2)
|
|
{
|
|
CurrentBuffer = 0;
|
|
}
|
|
|
|
EBufferReadMode DataReadMode;
|
|
if (bPlayedCachedBuffer)
|
|
{
|
|
bPlayedCachedBuffer = false;
|
|
DataReadMode = EBufferReadMode::AsynchronousSkipFirstFrame;
|
|
}
|
|
else
|
|
{
|
|
DataReadMode = EBufferReadMode::Asynchronous;
|
|
}
|
|
|
|
const bool bIsFinishedOrLooped = ReadMoreRealtimeData(DecompressionState, CurrentBuffer, DataReadMode);
|
|
|
|
// If this was a synchronous read, then immediately write it
|
|
if (AsyncRealtimeAudioTask == nullptr && !bHasError)
|
|
{
|
|
SubmitRealTimeSourceData(bIsFinishedOrLooped);
|
|
}
|
|
}
|
|
|
|
if (bDirectRendering)
|
|
{
|
|
check(CurrentBuffer == 0);
|
|
SubmitBuffer(SourceVoiceBuffers[CurrentBuffer]);
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::SubmitBuffer(TSharedPtr<FAlignedFloatBuffer, ESPMode::ThreadSafe> InSourceVoiceBuffer)
|
|
{
|
|
NumBuffersQeueued++;
|
|
BufferQueue.Enqueue(InSourceVoiceBuffer);
|
|
}
|
|
|
|
bool FMixerSourceBuffer::IsEndOfAudio() const
|
|
{
|
|
return bDirectRendering ? bBufferFinished : GetNumBuffersQueued() == 0;
|
|
}
|
|
|
|
void FMixerSourceBuffer::DeleteDecoder()
|
|
{
|
|
// Clean up decompression state after things have been finished using it
|
|
if (DecompressionState)
|
|
{
|
|
delete DecompressionState;
|
|
DecompressionState = nullptr;
|
|
}
|
|
}
|
|
|
|
bool FMixerSourceBuffer::OnBeginDestroy(USoundWave* /*Wave*/)
|
|
{
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
|
|
// if we don't have the lock, it means we are in ~FMixerSourceBuffer() on another thread
|
|
if (Lock.IsLocked() && SoundWave)
|
|
{
|
|
EnsureAsyncTaskFinishes();
|
|
DeleteDecoder();
|
|
ClearWave();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool FMixerSourceBuffer::OnIsReadyForFinishDestroy(USoundWave* /*Wave*/) const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
void FMixerSourceBuffer::OnFinishDestroy(USoundWave* /*Wave*/)
|
|
{
|
|
EnsureAsyncTaskFinishes();
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
|
|
// if we don't have the lock, it means we are in ~FMixerSourceBuffer() on another thread
|
|
if (Lock.IsLocked() && SoundWave)
|
|
{
|
|
DeleteDecoder();
|
|
ClearWave();
|
|
}
|
|
}
|
|
|
|
bool FMixerSourceBuffer::IsAsyncTaskInProgress() const
|
|
{
|
|
FScopeLock Lock(&DecodeTaskCritSec);
|
|
return AsyncRealtimeAudioTask != nullptr;
|
|
}
|
|
|
|
bool FMixerSourceBuffer::IsAsyncTaskDone() const
|
|
{
|
|
FScopeLock Lock(&DecodeTaskCritSec);
|
|
if (AsyncRealtimeAudioTask)
|
|
{
|
|
return AsyncRealtimeAudioTask->IsDone();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
float FMixerSourceBuffer::GetRelativeRenderCost() const
|
|
{
|
|
return RelativeRenderCost.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void FMixerSourceBuffer::SetRelativeRenderCost(float InRelativeRenderCost)
|
|
{
|
|
RelativeRenderCost.store(InRelativeRenderCost, std::memory_order_relaxed);
|
|
}
|
|
|
|
#if ENABLE_AUDIO_DEBUG
|
|
double FMixerSourceBuffer::GetCPUCoreUtilization() const
|
|
{
|
|
return CPUCoreUtilization.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void FMixerSourceBuffer::UpdateCPUCoreUtilization(double InCPUTime, double InAudioTime)
|
|
{
|
|
constexpr double AnalysisTime = 1.0;
|
|
|
|
if (InAudioTime > 0.0)
|
|
{
|
|
double NewUtilization = InCPUTime / InAudioTime;
|
|
|
|
// Determine smoothing coefficients based upon duration of audio being rendered.
|
|
const double DigitalCutoff = 1.0 / FMath::Max(1., AnalysisTime / InAudioTime);
|
|
const double SmoothingBeta = FMath::Clamp(FMath::Exp(-UE_PI * DigitalCutoff), 0.0, 1.0 - UE_DOUBLE_SMALL_NUMBER);
|
|
|
|
double PriorUtilization = CPUCoreUtilization.load(std::memory_order_relaxed);
|
|
|
|
// Smooth value if utilization has been initialized.
|
|
if (PriorUtilization > 0.0)
|
|
{
|
|
NewUtilization = (1.0 - SmoothingBeta) * NewUtilization + SmoothingBeta * PriorUtilization;
|
|
}
|
|
CPUCoreUtilization.store(NewUtilization, std::memory_order_relaxed);
|
|
}
|
|
}
|
|
#endif // ENABLE_AUDIO_DEBUG
|
|
|
|
void FMixerSourceBuffer::GetDiagnosticState(FDiagnosticState& OutState)
|
|
{
|
|
// Query without a lock!
|
|
OutState.bInFlight = AsyncRealtimeAudioTask != nullptr;
|
|
OutState.WaveName = WaveName;
|
|
OutState.bProcedural = bProcedural;
|
|
OutState.RunTimeInSecs = OutState.bInFlight ?
|
|
FPlatformTime::ToSeconds(FPlatformTime::Cycles64() - this->AsyncTaskStartTimeInCycles) :
|
|
0.f;
|
|
}
|
|
|
|
void FMixerSourceBuffer::EnsureAsyncTaskFinishes()
|
|
{
|
|
FScopeLock Lock(&DecodeTaskCritSec);
|
|
if (AsyncRealtimeAudioTask)
|
|
{
|
|
AsyncRealtimeAudioTask->CancelTask();
|
|
|
|
delete AsyncRealtimeAudioTask;
|
|
AsyncRealtimeAudioTask = nullptr;
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::OnBeginGenerate()
|
|
{
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
if (!Lock.IsLocked())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (SoundGenerator.IsValid())
|
|
{
|
|
SoundGenerator->OnBeginGenerate();
|
|
}
|
|
else
|
|
{
|
|
if (SoundWave && bProcedural)
|
|
{
|
|
check(SoundWave && SoundWave->bProcedural);
|
|
SoundWave->OnBeginGenerate();
|
|
}
|
|
}
|
|
}
|
|
|
|
void FMixerSourceBuffer::OnEndGenerate()
|
|
{
|
|
// Make sure the async task finishes!
|
|
EnsureAsyncTaskFinishes();
|
|
|
|
FScopeTryLock Lock(&SoundWaveCritSec);
|
|
if (!Lock.IsLocked())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (SoundGenerator.IsValid())
|
|
{
|
|
SoundGenerator->OnEndGenerate();
|
|
if (SoundWave)
|
|
{
|
|
SoundWave->OnEndGenerate(SoundGenerator);
|
|
}
|
|
|
|
if (FAudioDeviceManager* ADM = FAudioDeviceManager::Get())
|
|
{
|
|
if (FAudioDevice* AudioDevice = ADM->GetAudioDeviceRaw(AuioDeviceID))
|
|
{
|
|
UAudioBusSubsystem* AudioBusSubsystem = AudioDevice->GetSubsystem<UAudioBusSubsystem>();
|
|
check(AudioBusSubsystem);
|
|
AudioBusSubsystem->RemoveSound(InstanceID);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Only need to call OnEndGenerate and access SoundWave here if we successfully initialized
|
|
if (SoundWave && bInitialized && bProcedural)
|
|
{
|
|
check(SoundWave && SoundWave->bProcedural);
|
|
SoundWave->OnEndGenerate();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|