Files
UnrealEngine/Engine/Source/Runtime/AdpcmAudioDecoder/Module/Private/ADPCMAudioInfo.cpp
2025-05-18 13:04:45 +08:00

1048 lines
37 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Decoders/ADPCMAudioInfo.h"
#include "Interfaces/IAudioFormat.h"
#include "Adpcm.h"
static int32 bDisableADPCMSeekingCVar = 0;
FAutoConsoleVariableRef CVarDisableADPCMSeeking(
TEXT("au.adpcm.DisableSeeking"),
bDisableADPCMSeekingCVar,
TEXT("Disables seeking with ADPCM.\n"),
ECVF_Default);
static int32 ADPCMReadFailiureTimeoutCVar = 64;
FAutoConsoleVariableRef CVarADPCMReadFailiureTimeout(
TEXT("au.adpcm.ADPCMReadFailiureTimeout"),
ADPCMReadFailiureTimeoutCVar,
TEXT("Sets the number of ADPCM decode attempts we'll try before stopping the sound wave altogether.\n"),
ECVF_Default);
static int32 ADPCMDisableSeekForwardOnChunkMissesCVar = 1;
FAutoConsoleVariableRef CVarADPCMDisableSeekForwardOnChunkMisses(
TEXT("au.adpcm.DisableSeekForwardOnReadMisses"),
ADPCMDisableSeekForwardOnChunkMissesCVar,
TEXT("When there is a seek pending and this CVar is set to 0, we will scan forward in the file.\n"),
ECVF_Default);
static int32 ADPCMOnlySeekForwardOneChunkCVar = 1;
FAutoConsoleVariableRef CVarADPCMOnlySeekForwardOneChunk(
TEXT("au.adpcm.OnlySeekForwardOneChunk"),
ADPCMOnlySeekForwardOneChunkCVar,
TEXT("When set to 1, we will not continue to seek forward after failing to load two chunks in a row.\n"),
ECVF_Default);
static float ChanceForIntentionalChunkMissCVar = 0.0f;
FAutoConsoleVariableRef CVarChanceForIntentionalChunkMiss(
TEXT("au.adpcm.ChanceForIntentionalChunkMiss"),
ChanceForIntentionalChunkMissCVar,
TEXT("If this is set > 0 we will intentionally drop chunks. Used for debugging..\n"),
ECVF_Default);
FADPCMAudioInfo::FADPCMAudioInfo(void)
: NumConsecutiveReadFailiures(0)
, UncompressedBlockSize(0)
, CompressedBlockSize(0)
, BlockSize(0)
, StreamBufferSize(0)
, TotalDecodedSize(0)
, NumChannels(0)
, Format(0)
, PreviouslyRequestedChunkIndex(0)
, UncompressedBlockData(nullptr)
, SamplesPerBlock(0)
, FirstChunkSampleDataOffset(0)
, FirstChunkSampleDataIndex(0)
, bDecompressorReleased(false)
, bNewSeekRequest(false)
, bSeekPendingRead(false)
, bSeekedFowardToNextChunk(false)
, TargetSeekFrame(0)
{
}
FADPCMAudioInfo::~FADPCMAudioInfo(void)
{
if(UncompressedBlockData != nullptr)
{
FMemory::Free(UncompressedBlockData);
UncompressedBlockData = nullptr;
}
}
void FADPCMAudioInfo::SeekToTime(const float InSeekTime)
{
if (bDisableADPCMSeekingCVar)
{
return;
}
const uint32 SamplesPerSec = *WaveInfo.pSamplesPerSec;
TargetSeekFrame = static_cast<uint32>(InSeekTime * SamplesPerSec);
bNewSeekRequest = true;
}
void FADPCMAudioInfo::SeekToFrame(const uint32 InSeekFrame)
{
if (bDisableADPCMSeekingCVar)
{
return;
}
TargetSeekFrame = InSeekFrame;
bNewSeekRequest = true;
}
void FADPCMAudioInfo::SeekToFrameInternal(const uint32 InSeekFrame)
{
// Reset chunk handle in preperation for a new chunk.
CurCompressedChunkData = nullptr;
// zero out any existing decode data as it will stale after seeking
if (UncompressedBlockData)
{
FMemory::Memzero(UncompressedBlockData, NumChannels * UncompressedBlockSize);
}
float SeekTimeSeconds = InSeekFrame / static_cast<float>(*WaveInfo.pSamplesPerSec);
UE_LOG(LogAudio, Verbose, TEXT("Seeking ADPCM source to %.3f sec"), SeekTimeSeconds);
if (InSeekFrame == 0)
{
CurrentCompressedBlockIndex = 0;
// This is set to the max value to trigger the decompression of the first audio block
CurrentUncompressedBlockSampleIndex = UncompressedBlockSize / sizeof(uint16);;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
ResetSeekState();
return;
}
// Calculate block index & force SeekTime to be in bounds.
check(WaveInfo.pSamplesPerSec != nullptr);
uint32 SeekedSamples = InSeekFrame;
TotalSamplesStreamed = FMath::Min<uint32>(SeekedSamples, TotalSamplesPerChannel - 1);
const uint32 HeaderOffset = static_cast<uint32>(WaveInfo.SampleDataStart - SrcBufferData);
if (!StreamingSoundWave)
{
// For the non streaming case:
if (Format == WAVE_FORMAT_LPCM)
{
// There are no "blocks" on LPCM, so only update the total samples streamed (which is based off sample rate).
// Note that TotalSamplesStreamed is per-channel in the ReadCompressedInfo. Channels are taken into account there.
TotalSamplesStreamed = FMath::Clamp<uint32>(SeekedSamples, 0, TotalSamplesPerChannel - 1);
}
else
{
// Clamp to the end of memory in case we have an invalid seek time.
SeekedSamples = FMath::Clamp<uint32>(SeekedSamples, 0, TotalSamplesPerChannel - 1);
// Compute the block index that we're seeked to
CurrentCompressedBlockIndex = SeekedSamples / SamplesPerBlock;
// Update the samples streamed to the current block index and the samples per block
TotalSamplesStreamed = CurrentCompressedBlockIndex * SamplesPerBlock;
}
}
else
{
if (StreamingSoundWave->GetNumChunks() == 0)
{
UE_LOG(LogAudio, Error, TEXT("Entered streaming seek path with a non-streaming sound!"));
return;
}
const uint32 TotalStreamingChunks = StreamingSoundWave->GetNumChunks();
if (Format == WAVE_FORMAT_ADPCM)
{
CurrentCompressedBlockIndex = TotalSamplesStreamed / SamplesPerBlock; // Compute the block index that where SeekTime resides.
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
const int32 ChannelBlockSize = BlockSize * NumChannels;
for (uint32 BlockIndex = 0; BlockIndex < CurrentCompressedBlockIndex; ++BlockIndex)
{
const uint32 SizeOfChunk = StreamingSoundWave->GetSizeOfChunk(CurrentChunkIndex);
if (CurrentChunkBufferOffset + ChannelBlockSize >= SizeOfChunk)
{
const uint32 RemainderAfterEndOfChunk = CurrentChunkBufferOffset + ChannelBlockSize - SizeOfChunk;
ensureMsgf(RemainderAfterEndOfChunk == 0, TEXT("Found partial ADPCM block of %u samples- Please check FADPCMAudioFormat::SplitDataForStreaming for errors."), RemainderAfterEndOfChunk);
++CurrentChunkIndex;
CurrentChunkBufferOffset = 0;
}
else
{
// Always add chunks in NumChannels pairs
CurrentChunkBufferOffset += ChannelBlockSize;
}
if (CurrentChunkIndex >= TotalStreamingChunks)
{
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
break;
}
}
}
else if (Format == WAVE_FORMAT_LPCM)
{
const int32 ChannelBlockSize = sizeof(int16) * NumChannels;
const uint32 TotalByteSizeToSeek = TotalSamplesStreamed * ChannelBlockSize;
uint32 TargetChunkIndex = FirstChunkSampleDataIndex;
uint32 SizeOfTargetChunk = StreamingSoundWave->GetSizeOfChunk(TargetChunkIndex);
uint32 CurrentBytes = 0;
while (CurrentBytes + SizeOfTargetChunk < TotalByteSizeToSeek)
{
const bool TargetChunkInRange = TargetChunkIndex < TotalStreamingChunks;
ensure(TargetChunkInRange);
if (!TargetChunkInRange)
{
break;
}
++TargetChunkIndex;
CurrentBytes += SizeOfTargetChunk;
CurrentChunkBufferOffset -= SizeOfTargetChunk;
SizeOfTargetChunk = StreamingSoundWave->GetSizeOfChunk(TargetChunkIndex);
}
check(CurrentBytes <= TotalByteSizeToSeek);
CurrentChunkIndex = TargetChunkIndex;
CurrentChunkBufferOffset = TotalByteSizeToSeek - CurrentBytes;
CurrentChunkBufferOffset -= CurrentChunkBufferOffset % ChannelBlockSize;
}
else
{
// If we hit this, Format was invalid:
checkNoEntry();
return;
}
}
bSeekPendingRead = true;
}
bool FADPCMAudioInfo::ReadCompressedInfo(const uint8* InSrcBufferData, uint32 InSrcBufferDataSize, struct FSoundQualityInfo* QualityInfo)
{
if (!InSrcBufferData)
{
FString Name = QualityInfo ? QualityInfo->DebugName : TEXT("Unknown");
UE_LOG(LogAudio, Error, TEXT("FADPCMAudioInfo::ReadCompressedInfo: Failed to read compressed ADPCM audio from ('%s') because there was no resource data."), *Name);
return false;
}
SrcBufferData = InSrcBufferData;
SrcBufferDataSize = InSrcBufferDataSize;
void* FormatHeader;
if (!WaveInfo.ReadWaveInfo((uint8*)SrcBufferData, SrcBufferDataSize, nullptr, false, &FormatHeader))
{
UE_LOG(LogAudio, Error, TEXT("FADPCMAudioInfo::ReadCompressedInfo: WaveInfo.ReadWaveInfo Failed"));
return false;
}
Format = *WaveInfo.pFormatTag;
NumChannels = *WaveInfo.pChannels;
if (Format == WAVE_FORMAT_ADPCM)
{
ADPCM::ADPCMFormatHeader* ADPCMHeader = (ADPCM::ADPCMFormatHeader*)FormatHeader;
TotalSamplesPerChannel = ADPCMHeader->SamplesPerChannel;
SamplesPerBlock = ADPCMHeader->wSamplesPerBlock;
const uint32 PreambleSize = 7;
BlockSize = *WaveInfo.pBlockAlign;
// ADPCM starts with 2 uncompressed samples and then the remaining compressed sample data has 2 samples per byte
UncompressedBlockSize = (2 + (BlockSize - PreambleSize) * 2) * sizeof(int16);
CompressedBlockSize = BlockSize;
const uint32 uncompressedBlockSamples = (2 + (BlockSize - PreambleSize) * 2);
const uint32 targetBlocks = MONO_PCM_BUFFER_SAMPLES / uncompressedBlockSamples;
StreamBufferSize = targetBlocks * UncompressedBlockSize;
// Ensure TotalDecodedSize is a even multiple of the compressed block size so that the buffer is not over read on the last block
TotalDecodedSize = ((WaveInfo.SampleDataSize + CompressedBlockSize - 1) / CompressedBlockSize) * UncompressedBlockSize;
UncompressedBlockData = (uint8*)FMemory::Realloc(UncompressedBlockData, NumChannels * UncompressedBlockSize);
check(UncompressedBlockData != nullptr);
TotalCompressedBlocksPerChannel = (WaveInfo.SampleDataSize + CompressedBlockSize - 1) / CompressedBlockSize / NumChannels;
}
else if(Format == WAVE_FORMAT_LPCM)
{
// There are no "blocks" in this case
BlockSize = 0;
UncompressedBlockSize = 0;
CompressedBlockSize = 0;
StreamBufferSize = 0;
UncompressedBlockData = nullptr;
TotalCompressedBlocksPerChannel = 0;
TotalDecodedSize = WaveInfo.SampleDataSize;
TotalSamplesPerChannel = TotalDecodedSize / sizeof(uint16) / NumChannels;
}
else
{
UE_LOG(LogAudio, Error, TEXT("FADPCMAudioInfo::ReadCompressedInfo: Invalid format. Expected %d or %d. Got %d"), WAVE_FORMAT_LPCM, WAVE_FORMAT_ADPCM, Format);
return false;
}
if (QualityInfo)
{
QualityInfo->SampleRate = *WaveInfo.pSamplesPerSec;
QualityInfo->NumChannels = *WaveInfo.pChannels;
QualityInfo->SampleDataSize = TotalDecodedSize;
QualityInfo->Duration = (float)TotalSamplesPerChannel / QualityInfo->SampleRate;
}
CurrentCompressedBlockIndex = 0;
TotalSamplesStreamed = 0;
// This is set to the max value to trigger the decompression of the first audio block
CurrentUncompressedBlockSampleIndex = UncompressedBlockSize / sizeof(uint16);
return true;
}
bool FADPCMAudioInfo::ReadCompressedData(uint8* Destination, bool bLooping, uint32 BufferSize)
{
const uint32 ChannelSampleSize = sizeof(uint16) * NumChannels;
// This correctly handles any BufferSize as long as its a multiple of sample size * number of channels
check(Destination);
check(BufferSize % ChannelSampleSize == 0);
ProcessSeekRequest();
// If we've already read through this asset and we are not looping, memzero and early out.
if (TotalSamplesStreamed >= TotalSamplesPerChannel && !bLooping)
{
FMemory::Memzero(Destination, BufferSize);
return true;
}
int16* OutData = (int16*)Destination;
bool ReachedEndOfSamples = false;
if(Format == WAVE_FORMAT_ADPCM)
{
// We need to loop over the number of samples requested since an uncompressed block will not match the number of frames requested
while(BufferSize > 0)
{
if(CurrentUncompressedBlockSampleIndex >= UncompressedBlockSize / sizeof(uint16))
{
// we need to decompress another block of compressed data from the current chunk
// Decompress one block for each channel and store it in UncompressedBlockData
for(int32 ChannelItr = 0; ChannelItr < NumChannels; ++ChannelItr)
{
bool bSuccess = ADPCM::DecodeBlock(
WaveInfo.SampleDataStart + (ChannelItr * TotalCompressedBlocksPerChannel + CurrentCompressedBlockIndex) * CompressedBlockSize,
CompressedBlockSize,
(int16*)(UncompressedBlockData + ChannelItr * UncompressedBlockSize));
if (!bSuccess)
{
FMemory::Memzero(Destination, BufferSize);
bHasError = true;
return true;
}
}
// Update some bookkeeping
CurrentUncompressedBlockSampleIndex = 0;
++CurrentCompressedBlockIndex;
}
// Only copy over the number of samples we currently have available, we will loop around if needed
uint32 DecompressedSamplesToCopy = FMath::Min<uint32>(UncompressedBlockSize / sizeof(uint16) - CurrentUncompressedBlockSampleIndex, BufferSize / (sizeof(uint16) * NumChannels));
check(DecompressedSamplesToCopy > 0);
// Ensure we don't go over the number of samples left in the audio data
if(DecompressedSamplesToCopy > TotalSamplesPerChannel - TotalSamplesStreamed)
{
DecompressedSamplesToCopy = TotalSamplesPerChannel - TotalSamplesStreamed;
}
// Copy over the actual sample data
for(uint32 SampleItr = 0; SampleItr < DecompressedSamplesToCopy; ++SampleItr)
{
for(int32 ChannelItr = 0; ChannelItr < NumChannels; ++ChannelItr)
{
uint16 Value = *(int16*)(UncompressedBlockData + ChannelItr * UncompressedBlockSize + (CurrentUncompressedBlockSampleIndex + SampleItr) * sizeof(int16));
*OutData++ = Value;
}
}
// Update bookkeeping
CurrentUncompressedBlockSampleIndex += DecompressedSamplesToCopy;
BufferSize -= DecompressedSamplesToCopy * sizeof(uint16) * NumChannels;
TotalSamplesStreamed += DecompressedSamplesToCopy;
// Check for the end of the audio samples and loop if needed
if(TotalSamplesStreamed >= TotalSamplesPerChannel)
{
ReachedEndOfSamples = true;
if(!bLooping)
{
// Zero remaining buffer
FMemory::Memzero(OutData, BufferSize);
ResetSeekState();
return true;
}
else
{
// This is set to the max value to trigger the decompression of the first audio block
CurrentUncompressedBlockSampleIndex = UncompressedBlockSize / sizeof(uint16);
CurrentCompressedBlockIndex = 0;
TotalSamplesStreamed = 0;
}
}
}
}
else
{
uint32 OutDataOffset = 0;
while (BufferSize > 0)
{
uint32 DecompressedSamplesToCopy = BufferSize / ChannelSampleSize;
// Ensure we don't go over the number of samples left in the audio data
if (DecompressedSamplesToCopy > TotalSamplesPerChannel - TotalSamplesStreamed)
{
DecompressedSamplesToCopy = TotalSamplesPerChannel - TotalSamplesStreamed;
}
FMemory::Memcpy(OutData + OutDataOffset, WaveInfo.SampleDataStart + (TotalSamplesStreamed * ChannelSampleSize), DecompressedSamplesToCopy * ChannelSampleSize);
TotalSamplesStreamed += DecompressedSamplesToCopy;
BufferSize -= DecompressedSamplesToCopy * ChannelSampleSize;
OutDataOffset += DecompressedSamplesToCopy * NumChannels;
// Check for the end of the audio samples and loop if needed
if (TotalSamplesStreamed >= TotalSamplesPerChannel)
{
ReachedEndOfSamples = true;
TotalSamplesStreamed = 0;
if (!bLooping)
{
// Zero remaining buffer
FMemory::Memzero(OutData + OutDataOffset, BufferSize);
ResetSeekState();
return true;
}
}
}
}
ResetSeekState();
return ReachedEndOfSamples;
}
void FADPCMAudioInfo::ExpandFile(uint8* DstBuffer, struct FSoundQualityInfo* QualityInfo)
{
check(DstBuffer);
ReadCompressedData(DstBuffer, false, TotalDecodedSize);
}
int FADPCMAudioInfo::GetStreamBufferSize() const
{
return StreamBufferSize;
}
void FADPCMAudioInfo::ProcessSeekRequest()
{
if (bNewSeekRequest)
{
SeekToFrameInternal(TargetSeekFrame);
}
}
void FADPCMAudioInfo::ResetSeekState()
{
bSeekPendingRead = false;
bNewSeekRequest = false;
}
bool FADPCMAudioInfo::StreamCompressedInfoInternal(const FSoundWaveProxyPtr& InWaveProxy, struct FSoundQualityInfo* QualityInfo)
{
FScopeLock ScopeLock(&CurCompressedChunkHandleCriticalSection);
check(QualityInfo);
check(StreamingSoundWave == InWaveProxy);
if (!ensure(InWaveProxy.IsValid()))
{
return false;
}
CurrentChunkIndex = 0;
// Get the first chunk of audio data (should already be loaded)
uint8 const* ChunkData = GetLoadedChunk(InWaveProxy, CurrentChunkIndex, CurrentChunkDataSize);
if (ChunkData == nullptr)
{
UE_LOG(LogAudio, Warning, TEXT("FADPCMAudioInfo::StreamCompressedInfoInternal: Failed to get loaded chunk at index '%u'"), CurrentChunkIndex);
return false;
}
SrcBufferData = nullptr;
SrcBufferDataSize = 0;
void* FormatHeader = nullptr;
FString ErrorMsg;
constexpr bool bHeaderDataOnly = true;
if (!WaveInfo.ReadWaveInfo(static_cast<const uint8*>(ChunkData), CurrentChunkDataSize, &ErrorMsg, bHeaderDataOnly, &FormatHeader))
{
UE_LOG(LogAudio, Warning, TEXT("FADPCMAudioInfo::StreamCompressedInfoInternal: Failed to read WaveInfo: %s"), *ErrorMsg);
return false;
}
// if we only included the header in the zeroth chunk, skip to the next chunk.
const int32 SampleDataOffset = UE_PTRDIFF_TO_INT32(WaveInfo.SampleDataStart - ChunkData);
check(SampleDataOffset > 0);
if (((uint32)SampleDataOffset) >= CurrentChunkDataSize)
{
++CurrentChunkIndex;
ChunkData = GetLoadedChunk(InWaveProxy, CurrentChunkIndex, CurrentChunkDataSize);
FirstChunkSampleDataIndex = CurrentChunkIndex;
FirstChunkSampleDataOffset = 0;
}
else
{
FirstChunkSampleDataOffset = UE_PTRDIFF_TO_INT32(WaveInfo.SampleDataStart - ChunkData);
FirstChunkSampleDataIndex = 0;
}
PreviouslyRequestedChunkIndex = CurrentChunkIndex;
SrcBufferData = ChunkData;
CurrentChunkBufferOffset = 0;
CurCompressedChunkData = nullptr;
bDecompressorReleased = false;
CurrentUncompressedBlockSampleIndex = 0;
TotalSamplesStreamed = 0;
Format = *WaveInfo.pFormatTag;
NumChannels = *WaveInfo.pChannels;
if (Format == WAVE_FORMAT_ADPCM)
{
check(FormatHeader);
ADPCM::ADPCMFormatHeader* ADPCMHeader = (ADPCM::ADPCMFormatHeader*)FormatHeader;
TotalSamplesPerChannel = ADPCMHeader->SamplesPerChannel;
SamplesPerBlock = ADPCMHeader->wSamplesPerBlock;
const uint32 PreambleSize = 7;
BlockSize = *WaveInfo.pBlockAlign;
UncompressedBlockSize = (2 + (BlockSize - PreambleSize) * 2) * sizeof(int16);
CompressedBlockSize = BlockSize;
// Calculate buffer sizes and total number of samples
const uint32 uncompressedBlockSamples = (2 + (BlockSize - PreambleSize) * 2);
const uint32 targetBlocks = MONO_PCM_BUFFER_SAMPLES / uncompressedBlockSamples;
StreamBufferSize = targetBlocks * UncompressedBlockSize;
TotalDecodedSize = ((WaveInfo.SampleDataSize + CompressedBlockSize - 1) / CompressedBlockSize) * UncompressedBlockSize;
UncompressedBlockData = (uint8*)FMemory::Realloc(UncompressedBlockData, NumChannels * UncompressedBlockSize);
check(UncompressedBlockData != nullptr);
}
else if (Format == WAVE_FORMAT_LPCM)
{
BlockSize = 0;
UncompressedBlockSize = 0;
CompressedBlockSize = 0;
// This is uncompressed, so decoded size and buffer size are the same
TotalDecodedSize = WaveInfo.SampleDataSize;
StreamBufferSize = WaveInfo.SampleDataSize;
TotalSamplesPerChannel = StreamBufferSize / sizeof(uint16) / NumChannels;
}
else
{
UE_LOG(LogAudio, Error, TEXT("FADPCMAudioInfo::StreamCompressedInfoInternal: Unsupported wave format (Neither ADPCM nor LPCM)"));
return false;
}
if (QualityInfo)
{
QualityInfo->SampleRate = *WaveInfo.pSamplesPerSec;
QualityInfo->NumChannels = *WaveInfo.pChannels;
QualityInfo->SampleDataSize = TotalDecodedSize;
QualityInfo->Duration = (float)TotalSamplesPerChannel / QualityInfo->SampleRate;
}
return true;
}
bool FADPCMAudioInfo::StreamCompressedData(uint8* Destination, bool bLooping, uint32 BufferSize, int32& OutNumBytesStreamed)
{
FScopeLock ScopeLock(&CurCompressedChunkHandleCriticalSection);
int32 OriginalBufferSize = (int32)BufferSize;
OutNumBytesStreamed = 0;
// Initial sanity checks:
if (bDecompressorReleased)
{
UE_LOG(LogAudio, Error, TEXT("Stream Compressed Data called after release!"));
if (Destination != nullptr)
{
FMemory::Memzero(Destination, BufferSize);
}
return true;
}
if (Destination == nullptr || BufferSize == 0)
{
UE_LOG(LogAudio, Error, TEXT("Stream Compressed Info not called!"));
return false;
}
if (NumChannels == 0)
{
UE_LOG(LogAudio, Error, TEXT("Stream Compressed Info not called!"));
FMemory::Memzero(Destination, BufferSize);
return true;
}
// Destination samples are interlaced by channel, BufferSize is in bytes
const int32 ChannelSampleSize = sizeof(uint16) * NumChannels;
// Ensure that BuffserSize is a multiple of the sample size times the number of channels
if (BufferSize % ChannelSampleSize != 0)
{
UE_LOG(LogAudio, Error, TEXT("Invalid buffer size %d requested for %d channels."), BufferSize, NumChannels);
FMemory::Memzero(Destination, BufferSize);
return true;
}
ProcessSeekRequest();
int16* OutData = (int16*)Destination;
bool ReachedEndOfSamples = false;
if(Format == WAVE_FORMAT_ADPCM)
{
// We need to loop over the number of samples requested since an uncompressed block will not match the number of frames requested
while(BufferSize > 0)
{
if(CurCompressedChunkData == nullptr || CurrentUncompressedBlockSampleIndex >= UncompressedBlockSize / sizeof(uint16))
{
// we need to decompress another block of compressed data from the current chunk
if(CurCompressedChunkData == nullptr || CurrentChunkBufferOffset >= CurrentChunkDataSize)
{
// Get another chunk of compressed data from the streaming engine
// CurrentChunkIndex is used to keep track of the current chunk for loading/unloading by the streaming engine, but chunk 0 is
// preloaded so don't increment this when getting chunk 0, only later chunks. If previous chunk retrieval failed because it was
// not ready, don't re-increment the CurrentChunkIndex. Only increment if seek not pending as seek determines the current chunk index
// and invalidates CurCompressedChunkData.
if(CurCompressedChunkData != nullptr)
{
// If we have already seeked forward to the next chunk because the chunk didn't load in time for the render callback, we don't increment here.
// Otherwise, we increment to the next chunk index and reset our chunk offset to the beginning of the new chunk.
if (!bSeekedFowardToNextChunk)
{
// Ensure that we're either seeking or moving sequentially through the file.
check(bSeekPendingRead || CurrentChunkIndex == PreviouslyRequestedChunkIndex);
++CurrentChunkIndex;
// Set the current buffer offset accounting for the header in the first chunk
if (!bSeekPendingRead)
{
// Ensure that, if we hit this code, we are not at the beginning of the file.
check(CurrentChunkIndex != FirstChunkSampleDataIndex);
CurrentChunkBufferOffset = 0;
}
}
// If this flag was raised, clear it for the next time we move on to another chunk.
bSeekedFowardToNextChunk = false;
}
if (CurrentChunkIndex >= StreamingSoundWave->GetNumChunks())
{
// If this is the case, we've seeked to the end of the file.
ReachedEndOfSamples = true;
CurrentUncompressedBlockSampleIndex = 0;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if (!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
// Request the next chunk of data from the streaming engine
CurCompressedChunkData = GetLoadedChunk(StreamingSoundWave, CurrentChunkIndex, CurrentChunkDataSize);
if(CurCompressedChunkData == nullptr)
{
// We only need to worry about missing the stream chunk if we were seeking. Seeking might cause a bit of latency with chunk loading. That is expected.
if (!bSeekPendingRead)
{
// If we did not load chunk then bail. CurrentChunkIndex will not get incremented on the next callback so in effect another attempt will be made to fetch the chunk.
// Since audio streaming depends on the general data streaming mechanism used by other parts of the engine and new data is pre-fetched on the game thread, its
// possible a game thread stall can cause this.
UE_LOG(LogAudio, Verbose, TEXT("Missed Deadline chunk %d"), CurrentChunkIndex);
}
// If we have a seek pending, or we're in the middle of playing back the file, seek forward in the chunk offset to where we would have been.
// If we've already seeked forward one chunk and haven't loaded the next chunk in time and are skipping to the next one, we stop seeking to prevent snowballing chunk load requests.
const bool bAlreadySeekedFowardOneChunk = ADPCMOnlySeekForwardOneChunkCVar && bSeekedFowardToNextChunk;
const bool bShouldSeekForwardOnMissedChunk = !ADPCMDisableSeekForwardOnChunkMissesCVar && !bAlreadySeekedFowardOneChunk && (bSeekPendingRead || CurrentChunkIndex > FirstChunkSampleDataIndex);
if (bShouldSeekForwardOnMissedChunk)
{
const uint32 NumBlocksForCallback = BufferSize / (UncompressedBlockSize * NumChannels);
const uint32 NumCompressedBytesForCallback = NumBlocksForCallback * CompressedBlockSize * NumChannels;
uint32 NumDecompressedSamplesForCallback = BufferSize / (ChannelSampleSize);
if (NumDecompressedSamplesForCallback > TotalSamplesPerChannel - TotalSamplesStreamed)
{
// If this is the case, we've seeked to the end of the file.
ReachedEndOfSamples = true;
CurrentUncompressedBlockSampleIndex = 0;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if (!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
// Seek forward in the file the amount we would be rendering otherwise.
CurrentChunkBufferOffset += NumCompressedBytesForCallback;
TotalSamplesStreamed += NumDecompressedSamplesForCallback;
const uint32 SizeOfCurrentChunk = StreamingSoundWave->GetSizeOfChunk(CurrentChunkIndex);
if (CurrentChunkBufferOffset >= SizeOfCurrentChunk)
{
// If this is the case, we've seeked forward past the end of the chunk.
// Ensure we never hit this section of code twice, resulting in a double increment of the chunk index.
ensureAlways(!bSeekedFowardToNextChunk);
// Ensure that we are either seeking of moving sequentially through the file.
ensureAlwaysMsgf(bSeekPendingRead || CurrentChunkIndex == PreviouslyRequestedChunkIndex, TEXT("Failed to load a chunk of ADPCM audio for the entire duration that chunk of audio was supposed to be played."));
++CurrentChunkIndex;
CurrentChunkBufferOffset -= SizeOfCurrentChunk;
// Ensure that we have not moved past the size of our new chunk with our current callback.
ensureAlways(CurrentChunkBufferOffset < StreamingSoundWave->GetSizeOfChunk(CurrentChunkIndex));
bSeekedFowardToNextChunk = true;
if (!ensureMsgf(CurrentChunkBufferOffset % CompressedBlockSize == 0, TEXT("Error: seeked partway into an ADPCM block. Please check the above code, as well as FAudioFormatADPCM::SplitDataForStreaming for accuracy.")))
{
CurrentChunkBufferOffset = AlignDown(CurrentChunkBufferOffset, CompressedBlockSize);
}
if (CurrentChunkIndex >= StreamingSoundWave->GetNumChunks())
{
// If this is the case, we've seeked to the end of the file.
ReachedEndOfSamples = true;
CurrentUncompressedBlockSampleIndex = 0;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if (!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
}
}
// zero out remaining data and bail
FMemory::Memset(OutData, 0, BufferSize);
return false;
}
if (CurrentChunkIndex == FirstChunkSampleDataIndex && !bSeekPendingRead)
{
// If we're in the first chunk, set the current buffer offset accounting for the header.
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
}
ResetSeekState();
}
// Decompress one block for each channel and store it in UncompressedBlockData
for(int32 ChannelItr = 0; ChannelItr < NumChannels; ++ChannelItr)
{
bool bSuccess = ADPCM::DecodeBlock(
CurCompressedChunkData + CurrentChunkBufferOffset + ChannelItr * CompressedBlockSize,
CompressedBlockSize,
(int16*)(UncompressedBlockData + ChannelItr * UncompressedBlockSize));
if (!bSuccess)
{
FMemory::Memzero(Destination, BufferSize);
bHasError = true;
return true;
}
}
// Update some bookkeeping
CurrentUncompressedBlockSampleIndex = 0;
CurrentChunkBufferOffset += NumChannels * CompressedBlockSize;
}
// Only copy over the number of samples we currently have available, we will loop around if needed
uint32 DecompressedSamplesToCopy = FMath::Min<uint32>(
(UncompressedBlockSize / sizeof(uint16)) - CurrentUncompressedBlockSampleIndex,
BufferSize / (ChannelSampleSize));
check(DecompressedSamplesToCopy > 0);
// Ensure we don't go over the number of samples left in the audio data
if(DecompressedSamplesToCopy > TotalSamplesPerChannel - TotalSamplesStreamed)
{
DecompressedSamplesToCopy = TotalSamplesPerChannel - TotalSamplesStreamed;
}
// Copy over the actual sample data
for(uint32 SampleItr = 0; SampleItr < DecompressedSamplesToCopy; ++SampleItr)
{
for(int32 ChannelItr = 0; ChannelItr < NumChannels; ++ChannelItr)
{
uint16 Value = *(int16*)(UncompressedBlockData + ChannelItr * UncompressedBlockSize + (CurrentUncompressedBlockSampleIndex + SampleItr) * sizeof(int16));
*OutData++ = Value;
OutNumBytesStreamed += sizeof(int16);
}
}
// Update bookkeeping
CurrentUncompressedBlockSampleIndex += DecompressedSamplesToCopy;
BufferSize -= DecompressedSamplesToCopy * ChannelSampleSize;
TotalSamplesStreamed += DecompressedSamplesToCopy;
// Check for the end of the audio samples and loop if needed
if(TotalSamplesStreamed >= TotalSamplesPerChannel)
{
ReachedEndOfSamples = true;
CurrentUncompressedBlockSampleIndex = 0;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = 0;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if(!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
}
}
else
{
while(BufferSize > 0)
{
if(CurCompressedChunkData == nullptr || CurrentChunkBufferOffset >= CurrentChunkDataSize)
{
// Get another chunk of compressed data from the streaming engine
// CurrentChunkIndex is used to keep track of the current chunk for loading/unloading by the streaming engine but chunk 0
// is preloaded so we don't want to increment this when getting chunk 0, only later chunks
// Also, if we failed to get a previous chunk because it was not ready we don't want to re-increment the CurrentChunkIndex
if(CurCompressedChunkData != nullptr)
{
++CurrentChunkIndex;
}
// Check for the end of the audio samples and loop if needed
if (CurrentChunkIndex >= StreamingSoundWave->GetNumChunks())
{
ReachedEndOfSamples = true;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if (!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
// Request the next chunk of data from the streaming engine
CurCompressedChunkData = GetLoadedChunk(StreamingSoundWave, CurrentChunkIndex, CurrentChunkDataSize);
if(CurCompressedChunkData == nullptr)
{
// Only report missing the stream chunk if we were seeking. Seeking
// may cause a bit of latency with chunk loading, which is expected.
if (!bSeekPendingRead)
{
// CurrentChunkIndex will not get incremented on the next callback, effectively causing another attempt to fetch the chunk.
// Since audio streaming depends on the general data streaming mechanism used by other parts of the engine and new data is
// prefetched on the game tick thread, this may be caused by a game hitch.
UE_LOG(LogAudio, Verbose, TEXT("Missed streaming ADPCM deadline chunk %d"), CurrentChunkIndex);
}
FMemory::Memset(OutData, 0, BufferSize);
NumConsecutiveReadFailiures++;
const bool bReadAttemptTimedOut = NumConsecutiveReadFailiures > ADPCMReadFailiureTimeoutCVar;
UE_CLOG(bReadAttemptTimedOut, LogAudio, Warning, TEXT("ADPCM Audio Decode timed out."), bReadAttemptTimedOut);
return NumConsecutiveReadFailiures > ADPCMReadFailiureTimeoutCVar;
}
NumConsecutiveReadFailiures = 0;
// Set the current buffer offset accounting for the header in the first chunk
if (!bSeekPendingRead)
{
CurrentChunkBufferOffset = CurrentChunkIndex == FirstChunkSampleDataIndex ? FirstChunkSampleDataOffset : 0;
}
ResetSeekState();
}
uint32 DecompressedSamplesToCopy = FMath::Min<uint32>(
(CurrentChunkDataSize - CurrentChunkBufferOffset) / ChannelSampleSize,
BufferSize / ChannelSampleSize);
if (DecompressedSamplesToCopy == 0)
{
CurrentChunkBufferOffset = CurrentChunkDataSize;
continue;
}
// Ensure we don't go over the number of samples left in the audio data
if(DecompressedSamplesToCopy > TotalSamplesPerChannel - TotalSamplesStreamed)
{
DecompressedSamplesToCopy = TotalSamplesPerChannel - TotalSamplesStreamed;
}
const uint32 SampleSize = DecompressedSamplesToCopy * ChannelSampleSize;
FMemory::Memcpy(OutData, CurCompressedChunkData + CurrentChunkBufferOffset, SampleSize);
OutData += DecompressedSamplesToCopy * NumChannels;
OutNumBytesStreamed += DecompressedSamplesToCopy * NumChannels * sizeof(int16);
CurrentChunkBufferOffset += SampleSize;
BufferSize -= SampleSize;
TotalSamplesStreamed += DecompressedSamplesToCopy;
// Check for the end of the audio samples and loop if needed
if(TotalSamplesStreamed >= TotalSamplesPerChannel)
{
ReachedEndOfSamples = true;
CurrentChunkIndex = FirstChunkSampleDataIndex;
CurrentChunkBufferOffset = FirstChunkSampleDataOffset;
TotalSamplesStreamed = 0;
CurCompressedChunkData = nullptr;
if(!bLooping)
{
// Set the remaining buffer to 0
FMemory::Memset(OutData, 0, BufferSize);
return true;
}
}
}
}
return ReachedEndOfSamples;
}
bool FADPCMAudioInfo::ReleaseStreamChunk(bool bBlockUntilReleased)
{
ResetSeekState();
if (bBlockUntilReleased)
{
// Wait for any pending decode tasks to finish.
FScopeLock ScopeLock(&CurCompressedChunkHandleCriticalSection);
CurCompressedChunkHandle = FAudioChunkHandle();
CurrentChunkBufferOffset = 0;
CurCompressedChunkData = nullptr;
CurrentChunkDataSize = 0;
bDecompressorReleased = true;
return true;
}
else
{
// If the chunk isn't currently in use, reset the chunk handle.
if (CurCompressedChunkHandleCriticalSection.TryLock())
{
CurCompressedChunkHandle = FAudioChunkHandle();
CurrentChunkBufferOffset = 0;
CurCompressedChunkData = nullptr;
CurrentChunkDataSize = 0;
bDecompressorReleased = true;
CurCompressedChunkHandleCriticalSection.Unlock();
return true;
}
else
{
// Otherwise, do nothing and exit.
return false;
}
}
}
const uint8* FADPCMAudioInfo::GetLoadedChunk(const FSoundWaveProxyPtr& InSoundWave, uint32 ChunkIndex, uint32& OutChunkSize)
{
if (ChunkIndex != 0 && FMath::RandRange(0.0f, 1.0f) < ChanceForIntentionalChunkMissCVar)
{
UE_LOG(LogAudio, Display, TEXT("Intentionally dropping a chunk here."));
OutChunkSize = 0;
return nullptr;
}
if (!ensure(InSoundWave.IsValid()) || ChunkIndex >= InSoundWave->GetNumChunks())
{
OutChunkSize = 0;
return nullptr;
}
else if (ChunkIndex == 0)
{
TArrayView<const uint8> ZerothChunk = FSoundWaveProxy::GetZerothChunk(InSoundWave, true);
OutChunkSize = ZerothChunk.Num();
PreviouslyRequestedChunkIndex = ChunkIndex;
return ZerothChunk.GetData();
}
else
{
const bool bIsSeekingOrLooping = bSeekPendingRead || ChunkIndex == FirstChunkSampleDataIndex;
ensureAlwaysMsgf(bIsSeekingOrLooping || ChunkIndex == PreviouslyRequestedChunkIndex || ChunkIndex == PreviouslyRequestedChunkIndex + 1, TEXT("ADPCM playback error! We skipped from the end of chunk %d to the beginning of chunk %d."), PreviouslyRequestedChunkIndex, ChunkIndex);
CurCompressedChunkHandle = IStreamingManager::Get().GetAudioStreamingManager().GetLoadedChunk(InSoundWave, ChunkIndex, false, true);
OutChunkSize = CurCompressedChunkHandle.Num();
PreviouslyRequestedChunkIndex = ChunkIndex;
return CurCompressedChunkHandle.GetData();
}
}