source-engine/engine/audio/snd_wave_data.cpp

2395 lines
70 KiB
C++
Raw Permalink Normal View History

2020-04-23 00:56:21 +08:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//
//=============================================================================//
#include "audio_pch.h"
#include "datacache/idatacache.h"
#include "utllinkedlist.h"
#include "utldict.h"
#include "filesystem/IQueuedLoader.h"
#include "cdll_int.h"
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
extern IVEngineClient *engineClient;
extern IFileSystem *g_pFileSystem;
extern IDataCache *g_pDataCache;
extern double realtime;
// console streaming buffer implementation, appropriate for high latency and low memory
// shift this many buffers through the wave
#define STREAM_BUFFER_COUNT 2
// duration of audio samples per buffer, 200ms is 2x the worst frame rate (10Hz)
// the engine then has at least 400ms to deliver a new buffer or pop (assuming 2 buffers)
#define STREAM_BUFFER_TIME 0.200f
// force a single buffer when streaming waves smaller than this
#define STREAM_BUFFER_DATASIZE XBOX_DVD_ECC_SIZE
// PC single buffering implementation
// UNDONE: Allocate this in cache instead?
#define SINGLE_BUFFER_SIZE 16384
// Force a small cache for debugging cache issues.
// #define FORCE_SMALL_MEMORY_CACHE_SIZE ( 6 * 1024 * 1024 )
#define DEFAULT_WAV_MEMORY_CACHE ( 16 * 1024 * 1024 )
#define DEFAULT_XBOX_WAV_MEMORY_CACHE ( 16 * 1024 * 1024 )
#define TF_XBOX_WAV_MEMORY_CACHE ( 24 * 1024 * 1024 ) // Team Fortress uses a larger cache
// Dev builds will be missing soundcaches and hitch sometimes, we only care if its being properly launched from steam where sound caches should be complete.
ConVar snd_async_spew_blocking( "snd_async_spew_blocking", "1", 0, "Spew message to console any time async sound loading blocks on file i/o. ( 0=Off, 1=With -steam only, 2=Always" );
ConVar snd_async_spew( "snd_async_spew", "0", 0, "Spew all async sound reads, including success" );
ConVar snd_async_fullyasync( "snd_async_fullyasync", "0", 0, "All playback is fully async (sound doesn't play until data arrives)." );
ConVar snd_async_stream_spew( "snd_async_stream_spew", "0", 0, "Spew streaming info ( 0=Off, 1=streams, 2=buffers" );
static bool SndAsyncSpewBlocking()
{
int pref = snd_async_spew_blocking.GetInt();
return ( pref >= 2 ) || ( pref == 1 && CommandLine()->FindParm( "-steam" ) != 0 );
}
#define SndAlignReads() 1
void MaybeReportMissingWav( char const *wav );
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
struct asyncwaveparams_t
{
asyncwaveparams_t() : bPrefetch( false ), bCanBeQueued( false ) {}
FileNameHandle_t hFilename; // handle to sound item name (i.e. not with sound\ prefix)
int datasize;
int seekpos;
int alignment;
bool bPrefetch;
bool bCanBeQueued;
};
//-----------------------------------------------------------------------------
// Purpose: Builds a cache of the data bytes for a specific .wav file
//-----------------------------------------------------------------------------
class CAsyncWaveData
{
public:
explicit CAsyncWaveData();
// APIS required by CManagedDataCacheClient
void DestroyResource();
CAsyncWaveData *GetData();
unsigned int Size();
static void AsyncCallback( const FileAsyncRequest_t &asyncRequest, int numReadBytes, FSAsyncStatus_t err );
static void QueuedLoaderCallback( void *pContext, void *pContext2, const void *pData, int nSize, LoaderError_t loaderError );
static CAsyncWaveData *CreateResource( const asyncwaveparams_t &params );
static unsigned int EstimatedSize( const asyncwaveparams_t &params );
void OnAsyncCompleted( const FileAsyncRequest_t* asyncFilePtr, int numReadBytes, FSAsyncStatus_t err );
bool BlockingCopyData( void *destbuffer, int destbufsize, int startoffset, int count );
bool BlockingGetDataPointer( void **ppData );
void SetAsyncPriority( int priority );
void StartAsyncLoading( const asyncwaveparams_t& params );
bool GetPostProcessed();
void SetPostProcessed( bool proc );
bool IsCurrentlyLoading();
char const *GetFileName();
// Data
public:
int m_nDataSize; // bytes requested
int m_nReadSize; // bytes actually read
void *m_pvData; // target buffer
byte *m_pAlloc; // memory of buffer (base may not match)
FileAsyncRequest_t m_async;
FSAsyncControl_t m_hAsyncControl;
float m_start; // time at request invocation
float m_arrival; // time at data arrival
FileNameHandle_t m_hFileNameHandle;
int m_nBufferBytes; // size of any pre-allocated target buffer
BufferHandle_t m_hBuffer; // used to dequeue the buffer after lru
unsigned int m_bLoaded : 1;
unsigned int m_bMissing : 1;
unsigned int m_bPostProcessed : 1;
};
//-----------------------------------------------------------------------------
// Purpose: C'tor
//-----------------------------------------------------------------------------
CAsyncWaveData::CAsyncWaveData() :
m_nDataSize( 0 ),
m_nReadSize( 0 ),
m_pvData( 0 ),
m_pAlloc( 0 ),
m_hBuffer( INVALID_BUFFER_HANDLE ),
m_nBufferBytes( 0 ),
m_hAsyncControl( NULL ),
m_bLoaded( false ),
m_bMissing( false ),
m_start( 0.0 ),
m_arrival( 0.0 ),
m_bPostProcessed( false ),
m_hFileNameHandle( 0 )
{
}
//-----------------------------------------------------------------------------
// Purpose: // APIS required by CDataLRU
//-----------------------------------------------------------------------------
void CAsyncWaveData::DestroyResource()
{
if ( IsPC() )
{
if ( m_hAsyncControl )
{
if ( !m_bLoaded && !m_bMissing )
{
// NOTE: We CANNOT call AsyncAbort since if the file is actually being read we'll end
// up still getting a callback, but our this ptr (deleted below) will be feeefeee and we'll trash the heap
// pretty bad. So we call AsyncFinish, which will do a blocking read and will definitely succeed
// Block until we are finished
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
}
g_pFileSystem->AsyncRelease( m_hAsyncControl );
m_hAsyncControl = NULL;
}
}
if ( IsX360() )
{
if ( m_hAsyncControl )
{
if ( !m_bLoaded && !m_bMissing )
{
// force an abort
int errStatus = g_pFileSystem->AsyncAbort( m_hAsyncControl );
if ( errStatus != FSASYNC_ERR_UNKNOWNID )
{
// must wait for abort to finish before deallocating data
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
}
}
g_pFileSystem->AsyncRelease( m_hAsyncControl );
m_hAsyncControl = NULL;
}
if ( m_hBuffer != INVALID_BUFFER_HANDLE )
{
// hint the manager that this tracked buffer is invalid
wavedatacache->MarkBufferDiscarded( m_hBuffer );
}
}
// delete buffers
if ( IsPC() || !IsX360() )
{
g_pFileSystem->FreeOptimalReadBuffer( m_pAlloc );
}
else
{
delete [] m_pAlloc;
}
delete this;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : char const
//-----------------------------------------------------------------------------
char const *CAsyncWaveData::GetFileName()
{
static char sz[MAX_PATH];
if ( m_hFileNameHandle )
{
if ( g_pFileSystem->String( m_hFileNameHandle, sz, sizeof( sz ) ) )
{
return sz;
}
}
Assert( 0 );
return "";
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : CAsyncWaveData
//-----------------------------------------------------------------------------
CAsyncWaveData *CAsyncWaveData::GetData()
{
return this;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : unsigned int
//-----------------------------------------------------------------------------
unsigned int CAsyncWaveData::Size()
{
int size = sizeof( *this );
if ( IsPC() )
{
size += m_nDataSize;
}
if ( IsX360() )
{
// the data size can shrink during streaming near end of file
// need the real contant size of this object's allocations
size += m_nBufferBytes;
}
return size;
}
//-----------------------------------------------------------------------------
// Purpose: Static method for CDataLRU
// Input : &params -
// Output : CAsyncWaveData
//-----------------------------------------------------------------------------
CAsyncWaveData *CAsyncWaveData::CreateResource( const asyncwaveparams_t &params )
{
CAsyncWaveData *pData = new CAsyncWaveData;
Assert( pData );
if ( pData )
{
if ( IsX360() )
{
// create buffer now for re-use during streaming process
pData->m_nBufferBytes = AlignValue( params.datasize, params.alignment );
pData->m_pAlloc = new byte[pData->m_nBufferBytes];
pData->m_pvData = pData->m_pAlloc;
}
pData->StartAsyncLoading( params );
}
return pData;
}
//-----------------------------------------------------------------------------
// Purpose: Static method
// Input : &params -
// Output : static unsigned int
//-----------------------------------------------------------------------------
unsigned int CAsyncWaveData::EstimatedSize( const asyncwaveparams_t &params )
{
int size = sizeof( CAsyncWaveData );
if ( IsPC() )
{
size += params.datasize;
}
if ( IsX360() )
{
// the expected size of this object's allocations
size += AlignValue( params.datasize, params.alignment );
}
return size;
}
//-----------------------------------------------------------------------------
// Purpose: Static method, called by thread, don't call anything non-threadsafe from handler!!!
// Input : asyncFilePtr -
// numReadBytes -
// err -
//-----------------------------------------------------------------------------
void CAsyncWaveData::AsyncCallback(const FileAsyncRequest_t &asyncRequest, int numReadBytes, FSAsyncStatus_t err )
{
CAsyncWaveData *pObject = reinterpret_cast< CAsyncWaveData * >( asyncRequest.pContext );
Assert( pObject );
if ( pObject )
{
pObject->OnAsyncCompleted( &asyncRequest, numReadBytes, err );
}
}
//-----------------------------------------------------------------------------
// Purpose: Static method, called by thread, don't call anything non-threadsafe from handler!!!
//-----------------------------------------------------------------------------
void CAsyncWaveData::QueuedLoaderCallback( void *pContext, void *pContext2, const void *pData, int nSize, LoaderError_t loaderError )
{
CAsyncWaveData *pObject = reinterpret_cast< CAsyncWaveData * >( pContext );
Assert( pObject );
pObject->OnAsyncCompleted( NULL, nSize, loaderError == LOADERERROR_NONE ? FSASYNC_OK : FSASYNC_ERR_FILEOPEN );
}
//-----------------------------------------------------------------------------
// Purpose: NOTE: THIS IS CALLED FROM A THREAD SO YOU CAN'T CALL INTO ANYTHING NON-THREADSAFE
// such as CUtlSymbolTable/CUtlDict (many of the CUtl* are non-thread safe)!!!
// Input : asyncFilePtr -
// numReadBytes -
// err -
//-----------------------------------------------------------------------------
void CAsyncWaveData::OnAsyncCompleted( const FileAsyncRequest_t *asyncFilePtr, int numReadBytes, FSAsyncStatus_t err )
{
if ( IsPC() )
{
// Take hold of pointer (we can just use delete[] across .dlls because we are using a shared memory allocator...)
if ( err == FSASYNC_OK || err == FSASYNC_ERR_READING )
{
m_arrival = ( float )Plat_FloatTime();
// Take over ptr
m_pAlloc = ( byte * )asyncFilePtr->pData;
if ( SndAlignReads() )
{
m_async.nOffset = ( m_async.nBytes - m_nDataSize );
m_async.nBytes -= m_async.nOffset;
m_pvData = ((byte *)m_pAlloc) + m_async.nOffset;
m_nReadSize = numReadBytes - m_async.nOffset;
}
else
{
m_pvData = m_pAlloc;
m_nReadSize = numReadBytes;
}
// Needs to be post-processed
m_bPostProcessed = false;
// Finished loading
m_bLoaded = true;
}
else if ( err == FSASYNC_ERR_FILEOPEN )
{
// SEE NOTE IN FUNCTION COMMENT ABOVE!!!
// Tracker 22905, et al.
// Because this api gets called from the other thread, don't spew warning here as it can
// cause a crash in searching CUtlSymbolTables since they use a global var for a LessFunc context!!!
m_bMissing = true;
}
}
if ( IsX360() )
{
m_arrival = (float)Plat_FloatTime();
// possibly reading more than intended due to alignment restriction
m_nReadSize = numReadBytes;
if ( m_nReadSize > m_nDataSize )
{
// clamp to expected, extra data is unreliable
m_nReadSize = m_nDataSize;
}
if ( err != FSASYNC_OK )
{
// track as any error
m_bMissing = true;
}
if ( err != FSASYNC_ERR_FILEOPEN )
{
// some data got loaded
m_bLoaded = true;
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *destbuffer -
// destbufsize -
// startoffset -
// count -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWaveData::BlockingCopyData( void *destbuffer, int destbufsize, int startoffset, int count )
{
if ( !m_bLoaded )
{
Assert( m_hAsyncControl );
// Force it to finish
// It could finish between the above line and here, but the AsyncFinish call will just have a bogus id, not a big deal
if ( SndAsyncSpewBlocking() )
{
// Force it to finish
float st = ( float )Plat_FloatTime();
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
float ed = ( float )Plat_FloatTime();
Warning( "%f BCD: Async I/O Force %s (%8.2f msec / %8.2f msec total)\n", realtime, GetFileName(), 1000.0f * (float)( ed - st ), 1000.0f * (float)( m_arrival - m_start ) );
}
else
{
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
}
}
// notify on any error
if ( m_bMissing )
{
// Only warn once
m_bMissing = false;
char fn[MAX_PATH];
if ( g_pFileSystem->String( m_hFileNameHandle, fn, sizeof( fn ) ) )
{
MaybeReportMissingWav( fn );
}
}
if ( !m_bLoaded )
{
return false;
}
else if ( m_arrival != 0 && snd_async_spew.GetBool() )
{
DevMsg( "%f Async I/O Read successful %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( m_arrival - m_start ) );
m_arrival = 0;
}
// clamp requested to available
if ( count > m_nReadSize )
{
count = m_nReadSize - startoffset;
}
if ( count < 0 )
{
return false;
}
// Copy data from stream buffer
Q_memcpy( destbuffer, (char *)m_pvData + ( startoffset - m_async.nOffset ), count );
g_pFileSystem->AsyncRelease( m_hAsyncControl );
m_hAsyncControl = NULL;
return true;
}
bool CAsyncWaveData::IsCurrentlyLoading()
{
if ( m_bLoaded )
return true;
FSAsyncStatus_t status = g_pFileSystem->AsyncStatus( m_hAsyncControl );
if ( status == FSASYNC_STATUS_INPROGRESS || status == FSASYNC_OK )
return true;
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : **ppData -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWaveData::BlockingGetDataPointer( void **ppData )
{
Assert( ppData );
if ( !m_bLoaded )
{
// Force it to finish
// It could finish between the above line and here, but the AsyncFinish call will just have a bogus id, not a big deal
if ( SndAsyncSpewBlocking() )
{
float st = ( float )Plat_FloatTime();
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
float ed = ( float )Plat_FloatTime();
Warning( "%f BlockingGetDataPointer: Async I/O Force %s (%8.2f msec / %8.2f msec total )\n", realtime, GetFileName(), 1000.0f * (float)( ed - st ), 1000.0f * (float)( m_arrival - m_start ) );
}
else
{
g_pFileSystem->AsyncFinish( m_hAsyncControl, true );
}
}
// notify on any error
if ( m_bMissing )
{
// Only warn once
m_bMissing = false;
char fn[MAX_PATH];
if ( g_pFileSystem->String( m_hFileNameHandle, fn, sizeof( fn ) ) )
{
MaybeReportMissingWav( fn );
}
}
if ( !m_bLoaded )
{
return false;
}
else if ( m_arrival != 0 && snd_async_spew.GetBool() )
{
DevMsg( "%f Async I/O Read successful %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( m_arrival - m_start ) );
m_arrival = 0;
}
*ppData = m_pvData;
g_pFileSystem->AsyncRelease( m_hAsyncControl );
m_hAsyncControl = NULL;
return true;
}
void CAsyncWaveData::SetAsyncPriority( int priority )
{
if ( m_async.priority != priority )
{
m_async.priority = priority;
g_pFileSystem->AsyncSetPriority( m_hAsyncControl, m_async.priority );
if ( snd_async_spew.GetBool() )
{
DevMsg( "%f Async I/O Bumped priority for %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( Plat_FloatTime() - m_start ) );
}
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : params -
//-----------------------------------------------------------------------------
void CAsyncWaveData::StartAsyncLoading( const asyncwaveparams_t& params )
{
Assert( IsX360() || ( IsPC() && !m_bLoaded ) );
// expected to be relative to the sound\ dir
m_hFileNameHandle = params.hFilename;
// build the real filename
char szFilename[MAX_PATH];
Q_snprintf( szFilename, sizeof( szFilename ), "sound\\%s", GetFileName() );
int nPriority = 1;
if ( params.bPrefetch )
{
// lower the priority of prefetched sounds, so they don't block immediate sounds from being loaded
nPriority = 0;
}
if ( !IsX360() )
{
m_async.pData = NULL;
if ( SndAlignReads() )
{
m_async.nOffset = 0;
m_async.nBytes = params.seekpos + params.datasize;
}
else
{
m_async.nOffset = params.seekpos;
m_async.nBytes = params.datasize;
}
}
else
{
Assert( params.datasize > 0 );
// using explicit allocated buffer on xbox
m_async.pData = m_pvData;
m_async.nOffset = params.seekpos;
m_async.nBytes = AlignValue( params.datasize, params.alignment );
}
m_async.pfnCallback = AsyncCallback; // optional completion callback
m_async.pContext = (void *)this; // caller's unique context
m_async.priority = nPriority; // inter list priority, 0=lowest
m_async.flags = IsX360() ? 0 : FSASYNC_FLAGS_ALLOCNOFREE;
m_async.pszPathID = "GAME";
m_bLoaded = false;
m_bMissing = false;
m_nDataSize = params.datasize;
m_start = (float)Plat_FloatTime();
m_arrival = 0;
m_nReadSize = 0;
m_bPostProcessed = false;
// The async layer creates a copy of this string, ok to send a local reference
m_async.pszFilename = szFilename;
char szFullName[MAX_PATH];
if ( IsX360() && ( g_pFileSystem->GetDVDMode() == DVDMODE_STRICT ) )
{
// all audio is expected be in zips
// resolve to absolute name now, where path can be filtered to just the zips (fast find, no real i/o)
// otherwise the dvd will do a costly seek for each zip miss due to search path fall through
PathTypeQuery_t pathType;
if ( !g_pFileSystem->RelativePathToFullPath( m_async.pszFilename, m_async.pszPathID, szFullName, sizeof( szFullName ), FILTER_CULLNONPACK, &pathType ) )
{
// not found, do callback now to handle error
m_async.pfnCallback( m_async, 0, FSASYNC_ERR_FILEOPEN );
return;
}
m_async.pszFilename = szFullName;
}
if ( IsX360() && params.bCanBeQueued )
{
// queued loader takes over
LoaderJob_t loaderJob;
loaderJob.m_pFilename = m_async.pszFilename;
loaderJob.m_pPathID = m_async.pszPathID;
loaderJob.m_pCallback = QueuedLoaderCallback;
loaderJob.m_pContext = (void *)this;
loaderJob.m_Priority = LOADERPRIORITY_DURINGPRELOAD;
loaderJob.m_pTargetData = m_async.pData;
loaderJob.m_nBytesToRead = m_async.nBytes;
loaderJob.m_nStartOffset = m_async.nOffset;
g_pQueuedLoader->AddJob( &loaderJob );
return;
}
MEM_ALLOC_CREDIT();
// Commence async I/O
Assert( !m_hAsyncControl );
g_pFileSystem->AsyncRead( m_async, &m_hAsyncControl );
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWaveData::GetPostProcessed()
{
return m_bPostProcessed;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : proc -
//-----------------------------------------------------------------------------
void CAsyncWaveData::SetPostProcessed( bool proc )
{
m_bPostProcessed = proc;
}
//-----------------------------------------------------------------------------
// Purpose: Implements a cache of .wav / .mp3 data based on filename
//-----------------------------------------------------------------------------
class CAsyncWavDataCache : public IAsyncWavDataCache,
public CManagedDataCacheClient<CAsyncWaveData, asyncwaveparams_t>
{
public:
CAsyncWavDataCache();
~CAsyncWavDataCache() {}
virtual bool Init( unsigned int memSize );
virtual void Shutdown();
// implementation that treats file as monolithic
virtual memhandle_t AsyncLoadCache( char const *filename, int datasize, int startpos, bool bIsPrefetch = false );
virtual void PrefetchCache( char const *filename, int datasize, int startpos );
virtual bool CopyDataIntoMemory( char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed );
virtual bool CopyDataIntoMemory( memhandle_t& handle, char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed );
virtual void SetPostProcessed( memhandle_t handle, bool proc );
virtual void Unload( memhandle_t handle );
virtual bool GetDataPointer( memhandle_t& handle, char const *filename, int datasize, int startpos, void **pData, int copystartpos, bool *pbPostProcessed );
virtual bool IsDataLoadCompleted( memhandle_t handle, bool *pIsValid );
virtual void RestartDataLoad( memhandle_t* handle, char const *filename, int datasize, int startpos );
virtual bool IsDataLoadInProgress( memhandle_t handle );
// Xbox: alternate multi-buffer streaming implementation
virtual StreamHandle_t OpenStreamedLoad( char const *pFileName, int dataSize, int dataStart, int startPos, int loopPos, int bufferSize, int numBuffers, streamFlags_t flags );
virtual void CloseStreamedLoad( StreamHandle_t hStream );
virtual int CopyStreamedDataIntoMemory( StreamHandle_t hStream, void *pBuffer, int bufferSize, int copyStartPos, int bytesToCopy );
virtual bool IsStreamedDataReady( StreamHandle_t hStream );
virtual void MarkBufferDiscarded( BufferHandle_t hBuffer );
virtual void *GetStreamedDataPointer( StreamHandle_t hStream, bool bSync );
virtual void Flush();
virtual void OnMixBegin();
virtual void OnMixEnd();
void QueueUnlock( const memhandle_t &handle );
void SpewMemoryUsage( int level );
// Cache helpers
bool GetItemName( DataCacheClientID_t clientId, const void *pItem, char *pDest, unsigned nMaxLen );
private:
void Clear();
struct CacheEntry_t
{
CacheEntry_t() :
name( 0 ),
handle( 0 )
{
}
FileNameHandle_t name;
memhandle_t handle;
};
// tags the signature of a buffer inside a rb tree for faster than linear find
struct BufferEntry_t
{
FileNameHandle_t m_hName;
memhandle_t m_hWaveData;
int m_StartPos;
bool m_bCanBeShared;
};
static bool BufferHandleLessFunc( const BufferEntry_t& lhs, const BufferEntry_t& rhs )
{
if ( lhs.m_hName != rhs.m_hName )
{
return lhs.m_hName < rhs.m_hName;
}
if ( lhs.m_StartPos != rhs.m_StartPos )
{
return lhs.m_StartPos < rhs.m_StartPos;
}
return lhs.m_bCanBeShared < rhs.m_bCanBeShared;
}
CUtlRBTree< BufferEntry_t, BufferHandle_t > m_BufferList;
// encapsulates (n) buffers for a streamed wave object
struct StreamedEntry_t
{
FileNameHandle_t m_hName;
memhandle_t m_hWaveData[STREAM_BUFFER_COUNT];
int m_Front; // buffer index, forever incrementing
int m_NextStartPos; // predicted offset if mixing linearly
int m_DataSize; // length of the data set in bytes
int m_DataStart; // file offset where data set starts
int m_LoopStart; // offset in data set where loop starts
int m_BufferSize; // size of the buffer in bytes
int m_numBuffers; // number of buffers (1 or 2) to march through
int m_SectorSize; // size of sector on stream device
bool m_bSinglePlay; // hint to keep same buffers
};
CUtlLinkedList< StreamedEntry_t, StreamHandle_t > m_StreamedHandles;
static bool CacheHandleLessFunc( const CacheEntry_t& lhs, const CacheEntry_t& rhs )
{
return lhs.name < rhs.name;
}
CUtlRBTree< CacheEntry_t, int > m_CacheHandles;
memhandle_t FindOrCreateBuffer( asyncwaveparams_t &params, bool bFind );
bool m_bInitialized;
bool m_bQueueCacheUnlocks;
CUtlVector<memhandle_t> m_unlockQueue;
};
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CAsyncWavDataCache::CAsyncWavDataCache() :
m_CacheHandles( 0, 0, CacheHandleLessFunc ),
m_BufferList( 0, 0, BufferHandleLessFunc ),
m_bInitialized( false ),
m_bQueueCacheUnlocks( false )
{
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::Init( unsigned int memSize )
{
if ( m_bInitialized )
return true;
if ( IsX360() )
{
const char *pGame = engineClient->GetGameDirectory();
if ( !Q_stricmp( Q_UnqualifiedFileName( pGame ), "tf" ) )
{
memSize = TF_XBOX_WAV_MEMORY_CACHE;
}
else
{
memSize = DEFAULT_XBOX_WAV_MEMORY_CACHE;
}
}
else
{
if ( memSize < DEFAULT_WAV_MEMORY_CACHE )
{
memSize = DEFAULT_WAV_MEMORY_CACHE;
}
}
#if FORCE_SMALL_MEMORY_CACHE_SIZE
memSize = FORCE_SMALL_MEMORY_CACHE_SIZE;
Msg( "WARNING CAsyncWavDataCache::Init() forcing small memory cache size: %u\n", memSize );
#endif
CCacheClientBaseClass::Init( g_pDataCache, "WaveData", memSize );
m_bInitialized = true;
return true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::Shutdown()
{
if ( !m_bInitialized )
{
return;
}
Clear();
CCacheClientBaseClass::Shutdown();
m_bInitialized = false;
}
//-----------------------------------------------------------------------------
// Purpose: Creates initial cache object if it doesn't already exist, starts async loading the actual data
// in any case.
// Input : *filename -
// datasize -
// startpos -
// Output : memhandle_t
//-----------------------------------------------------------------------------
memhandle_t CAsyncWavDataCache::AsyncLoadCache( char const *filename, int datasize, int startpos, bool bIsPrefetch )
{
VPROF( "CAsyncWavDataCache::AsyncLoadCache" );
FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename );
CacheEntry_t search;
search.name = fnh;
search.handle = 0;
// find or create the handle
int idx = m_CacheHandles.Find( search );
if ( idx == m_CacheHandles.InvalidIndex() )
{
idx = m_CacheHandles.Insert( search );
Assert( idx != m_CacheHandles.InvalidIndex() );
}
CacheEntry_t &entry = m_CacheHandles[idx];
// Try and pull it into cache
CAsyncWaveData *data = CacheGet( entry.handle );
if ( !data )
{
// Try and reload it
asyncwaveparams_t params;
params.hFilename = fnh;
params.datasize = datasize;
params.seekpos = startpos;
params.bPrefetch = bIsPrefetch;
entry.handle = CacheCreate( params );
}
return entry.handle;
}
//-----------------------------------------------------------------------------
// Purpose: Reclaim a buffer. A reclaimed resident buffer is ready for play.
//-----------------------------------------------------------------------------
memhandle_t CAsyncWavDataCache::FindOrCreateBuffer( asyncwaveparams_t &params, bool bFind )
{
CAsyncWaveData *pWaveData;
BufferEntry_t search;
BufferHandle_t hBuffer;
search.m_hName = params.hFilename;
search.m_StartPos = params.seekpos;
search.m_bCanBeShared = bFind;
search.m_hWaveData = 0;
if ( bFind )
{
// look for an existing buffer that matches exactly (same file, offset, and share)
int iBuffer = m_BufferList.Find( search );
if ( iBuffer != m_BufferList.InvalidIndex() )
{
// found
search.m_hWaveData = m_BufferList[iBuffer].m_hWaveData;
if ( snd_async_stream_spew.GetInt() >= 2 )
{
char tempBuff[MAX_PATH];
g_pFileSystem->String( params.hFilename, tempBuff, sizeof( tempBuff ) );
Msg( "Found Buffer: %s, offset: %d\n", tempBuff, params.seekpos );
}
}
}
// each resource buffer stays locked (valid) while in use
// a buffering stream is not subject to lru and can rely on it's buffers
// a buffering stream may obsolete it's buffers by reducing the lock count, allowing for lru
pWaveData = CacheLock( search.m_hWaveData );
if ( !pWaveData )
{
// not in cache, create and lock
// not found, create buffer and fill with data
search.m_hWaveData = CacheCreate( params, DCAF_LOCK );
// add the buffer to our managed list
hBuffer = m_BufferList.Insert( search );
Assert( hBuffer != m_BufferList.InvalidIndex() );
// store the handle into our managed list
// used during a lru discard as a means to keep the list in-sync
pWaveData = CacheGet( search.m_hWaveData );
pWaveData->m_hBuffer = hBuffer;
}
else
{
// still in cache
// same as requesting it and having it arrive instantly
pWaveData->m_start = pWaveData->m_arrival = (float)Plat_FloatTime();
}
return search.m_hWaveData;
}
//-----------------------------------------------------------------------------
// Purpose: Load data asynchronously via multi-buffers, returns specialized handle
//-----------------------------------------------------------------------------
StreamHandle_t CAsyncWavDataCache::OpenStreamedLoad( char const *pFileName, int dataSize, int dataStart, int startPos, int loopPos, int bufferSize, int numBuffers, streamFlags_t flags )
{
VPROF( "CAsyncWavDataCache::OpenStreamedLoad" );
StreamedEntry_t streamedEntry;
StreamHandle_t hStream;
asyncwaveparams_t params;
int i;
Assert( numBuffers > 0 && numBuffers <= STREAM_BUFFER_COUNT );
// queued load mandates one buffer
Assert( !( flags & STREAMED_QUEUEDLOAD ) || numBuffers == 1 );
streamedEntry.m_hName = g_pFileSystem->FindOrAddFileName( pFileName );
streamedEntry.m_Front = 0;
streamedEntry.m_DataSize = dataSize;
streamedEntry.m_DataStart = dataStart;
streamedEntry.m_NextStartPos = startPos + numBuffers * bufferSize;
streamedEntry.m_LoopStart = loopPos;
streamedEntry.m_BufferSize = bufferSize;
streamedEntry.m_numBuffers = numBuffers;
streamedEntry.m_bSinglePlay = ( flags & STREAMED_SINGLEPLAY ) != 0;
streamedEntry.m_SectorSize = ( IsX360() && ( flags & STREAMED_FROMDVD ) ) ? XBOX_DVD_SECTORSIZE : 1;
// single play streams expect to uniquely own and thus recycle their buffers though the data
// single play streams are guaranteed that their buffers are private and cannot be shared
// a non-single play stream wants persisting buffers and attempts to reclaim a matching buffer
bool bFindBuffer = ( streamedEntry.m_bSinglePlay == false );
// initial load populates buffers
// mixing starts after front buffer viable
// buffer rotation occurs after front buffer consumed
// there should be no blocking
params.hFilename = streamedEntry.m_hName;
params.datasize = bufferSize;
params.alignment = streamedEntry.m_SectorSize;
params.bCanBeQueued = ( flags & STREAMED_QUEUEDLOAD ) != 0;
for ( i=0; i<numBuffers; ++i )
{
params.seekpos = dataStart + startPos + i * bufferSize;
streamedEntry.m_hWaveData[i] = FindOrCreateBuffer( params, bFindBuffer );
}
// get a unique handle for each stream request
hStream = m_StreamedHandles.AddToTail( streamedEntry );
Assert( hStream != m_StreamedHandles.InvalidIndex() );
return hStream;
}
//-----------------------------------------------------------------------------
// Purpose: Cleanup a streamed load's resources.
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::CloseStreamedLoad( StreamHandle_t hStream )
{
VPROF( "CAsyncWavDataCache::CloseStreamedLoad" );
if ( hStream == INVALID_STREAM_HANDLE )
{
return;
}
int lockCount;
StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream];
for ( int i=0; i<streamedEntry.m_numBuffers; ++i )
{
// multiple streams could be using the same buffer, keeping the lock count nonzero
lockCount = GetCacheSection()->GetLockCount( streamedEntry.m_hWaveData[i] );
Assert( lockCount >= 1 );
if ( lockCount > 0 )
{
lockCount = CacheUnlock( streamedEntry.m_hWaveData[i] );
}
if ( streamedEntry.m_bSinglePlay )
{
// a buffering single play stream has no reason to reuse its own buffers and destroys them
Assert( lockCount == 0 );
CacheRemove( streamedEntry.m_hWaveData[i] );
}
}
m_StreamedHandles.Remove( hStream );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *filename -
// datasize -
// startpos -
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::PrefetchCache( char const *filename, int datasize, int startpos )
{
// Just do an async load, but don't get cache handle
AsyncLoadCache( filename, datasize, startpos, true );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *filename -
// datasize -
// startpos -
// *buffer -
// bufsize -
// copystartpos -
// bytestocopy -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::CopyDataIntoMemory( char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed )
{
VPROF( "CAsyncWavDataCache::CopyDataIntoMemory" );
bool bret = false;
// Add to caching system
AsyncLoadCache( filename, datasize, startpos );
FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename );
CacheEntry_t search;
search.name = fnh;
search.handle = 0;
// Now look it up, it should be in the system
int idx = m_CacheHandles.Find( search );
if ( idx == m_CacheHandles.InvalidIndex() )
{
Assert( 0 );
return bret;
}
// Now see if the handle has been paged out...
return CopyDataIntoMemory( m_CacheHandles[ idx ].handle, filename, datasize, startpos, buffer, bufsize, copystartpos, bytestocopy, pbPostProcessed );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : handle -
// *filename -
// datasize -
// startpos -
// *buffer -
// bufsize -
// copystartpos -
// bytestocopy -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::CopyDataIntoMemory( memhandle_t& handle, char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed )
{
VPROF( "CAsyncWavDataCache::CopyDataIntoMemory" );
*pbPostProcessed = false;
bool bret = false;
CAsyncWaveData *data = CacheLock( handle );
if ( !data )
{
FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename );
CacheEntry_t search;
search.name = fnh;
search.handle = 0;
// Now look it up, it should be in the system
int idx = m_CacheHandles.Find( search );
if ( idx == m_CacheHandles.InvalidIndex() )
{
Assert( 0 );
return false;
}
// Try and reload it
asyncwaveparams_t params;
params.hFilename = fnh;
params.datasize = datasize;
params.seekpos = startpos;
handle = m_CacheHandles[ idx ].handle = CacheCreate( params );
data = CacheLock( handle );
if ( !data )
{
return bret;
}
}
// Cache entry exists, but if filesize == 0 then the file itself wasn't on disk...
if ( data->m_nDataSize != 0 )
{
bret = data->BlockingCopyData( buffer, bufsize, copystartpos, bytestocopy );
}
*pbPostProcessed = data->GetPostProcessed();
// Release lock
CacheUnlock( handle );
return bret;
}
//-----------------------------------------------------------------------------
// Purpose: Copy from streaming buffers into target memory, never blocks.
//-----------------------------------------------------------------------------
int CAsyncWavDataCache::CopyStreamedDataIntoMemory( int hStream, void *pBuffer, int bufferSize, int copyStartPos, int bytesToCopy )
{
VPROF( "CAsyncWavDataCache::CopyStreamedDataIntoMemory" );
int actualCopied;
int count;
int i;
int which;
CAsyncWaveData *pWaveData[STREAM_BUFFER_COUNT];
CAsyncWaveData *pFront;
asyncwaveparams_t params;
int nextStartPos;
int bufferPos;
bool bEndOfFile;
int index;
bool bWaiting;
bool bCompleted;
StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream];
if ( copyStartPos >= streamedEntry.m_DataStart + streamedEntry.m_DataSize )
{
// at or past end of file
return 0;
}
for ( i=0; i<streamedEntry.m_numBuffers; ++i )
{
pWaveData[i] = CacheGetNoTouch( streamedEntry.m_hWaveData[i] );
Assert( pWaveData[i] );
}
// drive the buffering
index = streamedEntry.m_Front;
bEndOfFile = 0;
actualCopied = 0;
bWaiting = false;
while ( 1 )
{
// try to satisfy from the front
pFront = pWaveData[index % streamedEntry.m_numBuffers];
bufferPos = copyStartPos - pFront->m_async.nOffset;
// cache atomic async completion signal off to avoid coherency issues
bCompleted = pFront->m_bLoaded || pFront->m_bMissing;
if ( snd_async_stream_spew.GetInt() >= 1 )
{
// interval is the audio block clock rate, the block must be available within this interval
// a faster audio rate or smaller block size implies a smaller interval
// latency is the actual block delivery time
// latency must not exceed the delivery interval or stariving occurs and audio pops
float nowTime = Plat_FloatTime();
int interval = (int)(1000.0f*(nowTime-pFront->m_start));
int latency;
if ( bCompleted && pFront->m_bLoaded )
{
latency = (int)(1000.0f*(pFront->m_arrival-pFront->m_start));
}
else
{
// buffer has not arrived yet
latency = -1;
}
DevMsg( "Stream:%2d interval:%5dms latency:%5dms offset:%d length:%d (%s)\n", hStream, interval, latency, pFront->m_async.nOffset, pFront->m_nReadSize, pFront->GetFileName() );
}
if ( bCompleted && pFront->m_hAsyncControl && ( pFront->m_bLoaded || pFront->m_bMissing) )
{
g_pFileSystem->AsyncRelease( pFront->m_hAsyncControl );
pFront->m_hAsyncControl = NULL;
}
if ( bCompleted && pFront->m_bLoaded )
{
if ( bufferPos >= 0 && bufferPos < pFront->m_nReadSize )
{
count = bytesToCopy;
if ( bufferPos + bytesToCopy > pFront->m_nReadSize )
{
// clamp requested to actual available
count = pFront->m_nReadSize - bufferPos;
}
if ( bufferPos + count > bufferSize )
{
// clamp requested to caller's buffer dimension
count = bufferSize - bufferPos;
}
Q_memcpy( pBuffer, (char *)pFront->m_pvData + bufferPos, count );
// advance past consumed bytes
actualCopied += count;
copyStartPos += count;
bufferPos += count;
}
}
else if ( bCompleted && pFront->m_bMissing )
{
// notify on any error
MaybeReportMissingWav( pFront->GetFileName() );
break;
}
else
{
// data not available
bWaiting = true;
break;
}
// cycle past obsolete or consumed buffers
if ( bufferPos < 0 || bufferPos >= pFront->m_nReadSize )
{
// move to next buffer
index++;
if ( index - streamedEntry.m_Front >= streamedEntry.m_numBuffers )
{
// out of buffers
break;
}
}
if ( actualCopied == bytesToCopy )
{
// satisfied request
break;
}
}
if ( streamedEntry.m_numBuffers > 1 )
{
// restart consumed buffers
while ( streamedEntry.m_Front < index )
{
if ( !actualCopied && !bWaiting )
{
// couldn't return any data because the buffers aren't in the right location
// oh no! caller must be skipping
// due to latency the next buffer position has to start one full buffer ahead of the caller's desired read location
// hopefully only 1 buffer will stutter
nextStartPos = copyStartPos - streamedEntry.m_DataStart + streamedEntry.m_BufferSize;
// advance past, ready for next possible iteration
copyStartPos += streamedEntry.m_BufferSize;
}
else
{
// get the next forecasted read location
nextStartPos = streamedEntry.m_NextStartPos;
}
if ( nextStartPos >= streamedEntry.m_DataSize )
{
// next buffer is at or past end of file
if ( streamedEntry.m_LoopStart >= 0 )
{
// wrap back around to loop position
nextStartPos = streamedEntry.m_LoopStart;
}
else
{
// advance past consumed buffer
streamedEntry.m_Front++;
// start no further buffers
break;
}
}
// still valid data left to read
// snap the buffer position to required alignment
nextStartPos = streamedEntry.m_SectorSize * (nextStartPos/streamedEntry.m_SectorSize);
// start loading back buffer at future location
params.hFilename = streamedEntry.m_hName;
params.seekpos = streamedEntry.m_DataStart + nextStartPos;
params.datasize = streamedEntry.m_DataSize - nextStartPos;
params.alignment = streamedEntry.m_SectorSize;
if ( params.datasize > streamedEntry.m_BufferSize )
{
// clamp to buffer size
params.datasize = streamedEntry.m_BufferSize;
}
// save next start position
streamedEntry.m_NextStartPos = nextStartPos + params.datasize;
which = streamedEntry.m_Front % streamedEntry.m_numBuffers;
if ( streamedEntry.m_bSinglePlay )
{
// a single play wave has no reason to persist its buffers into the lru
// reuse buffer and restart until finished
pWaveData[which]->StartAsyncLoading( params );
}
else
{
// release obsolete buffer to lru management
CacheUnlock( streamedEntry.m_hWaveData[which] );
// reclaim or create/load the desired buffer
streamedEntry.m_hWaveData[which] = FindOrCreateBuffer( params, true );
}
streamedEntry.m_Front++;
}
if ( bWaiting )
{
// oh no! data needed is not yet available in front buffer
// caller requesting data faster than can be provided or caller skipped
// can only return what has been copied thus far (could be 0)
return actualCopied;
}
}
return actualCopied;
}
//-----------------------------------------------------------------------------
// Purpose: Get the front buffer, optionally block.
// Intended for user of a single buffer stream.
//-----------------------------------------------------------------------------
void *CAsyncWavDataCache::GetStreamedDataPointer( StreamHandle_t hStream, bool bSync )
{
void *pData;
CAsyncWaveData *pFront;
int index;
StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream];
index = streamedEntry.m_Front % streamedEntry.m_numBuffers;
pFront = CacheGetNoTouch( streamedEntry.m_hWaveData[index] );
Assert( pFront );
if ( !pFront )
{
// shouldn't happen
return NULL;
}
if ( !pFront->m_bMissing && pFront->m_bLoaded )
{
return pFront->m_pvData;
}
if ( bSync && pFront->BlockingGetDataPointer( &pData ) )
{
return pData;
}
return NULL;
}
//-----------------------------------------------------------------------------
// Purpose: The front buffer must be valid
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::IsStreamedDataReady( int hStream )
{
VPROF( "CAsyncWavDataCache::IsStreamedDataReady" );
if ( hStream == INVALID_STREAM_HANDLE )
{
return false;
}
StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream];
if ( streamedEntry.m_Front )
{
// already streaming, the buffers better be arriving as expected
return true;
}
// only the first front buffer must be present
CAsyncWaveData *pFront = CacheGetNoTouch( streamedEntry.m_hWaveData[0] );
Assert( pFront );
if ( !pFront )
{
// shouldn't happen
// let the caller think data is ready, so stream can shutdown
return true;
}
// regardless of any errors
// errors handled during data fetch
return pFront->m_bLoaded || pFront->m_bMissing;
}
//-----------------------------------------------------------------------------
// Purpose: Dequeue the buffer entry (backdoor for list management)
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::MarkBufferDiscarded( BufferHandle_t hBuffer )
{
m_BufferList.RemoveAt( hBuffer );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : handle -
// proc -
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::SetPostProcessed( memhandle_t handle, bool proc )
{
CAsyncWaveData *data = CacheGet( handle );
if ( data )
{
data->SetPostProcessed( proc );
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : handle -
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::Unload( memhandle_t handle )
{
// Don't actually unload, just mark it as stale
if ( GetCacheSection() )
{
GetCacheSection()->Age( handle );
}
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : handle -
// *filename -
// datasize -
// startpos -
// **pData -
// copystartpos -
// *pbPostProcessed -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::GetDataPointer( memhandle_t& handle, char const *filename, int datasize, int startpos, void **pData, int copystartpos, bool *pbPostProcessed )
{
VPROF( "CAsyncWavDataCache::GetDataPointer" );
Assert( pbPostProcessed );
Assert( pData );
*pbPostProcessed = false;
bool bret = false;
*pData = NULL;
CAsyncWaveData *data = CacheLock( handle );
if ( !data )
{
FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename );
CacheEntry_t search;
search.name = fnh;
search.handle = 0;
int idx = m_CacheHandles.Find( search );
if ( idx == m_CacheHandles.InvalidIndex() )
{
Assert( 0 );
return bret;
}
// Try and reload it
asyncwaveparams_t params;
params.hFilename = fnh;
params.datasize = datasize;
params.seekpos = startpos;
handle = m_CacheHandles[ idx ].handle = CacheCreate( params );
data = CacheLock( handle );
if ( !data )
{
return bret;
}
}
// Cache entry exists, but if filesize == 0 then the file itself wasn't on disk...
if ( data->m_nDataSize != 0 )
{
if ( datasize != data->m_nDataSize )
{
// We've had issues where we are called with datasize larger than what we read on disk.
// Ie: datasize is 277,180, data->m_nDataSize is 263,168
// This can happen due to a corrupted audio cache, but it's more likely that somehow
// we wound up reading the cache data from one language and the file from another.
DevMsg( "Cached datasize != sound datasize %d - %d.\n", datasize, data->m_nDataSize );
#ifdef STAGING_ONLY
// Adding a STAGING_ONLY debugger break to try and help track this down. Hopefully we'll
// get this crash internally with full debug information instead of just minidump files.
DebuggerBreak();
#endif
}
else if ( copystartpos < data->m_nDataSize )
{
if ( data->BlockingGetDataPointer( pData ) )
{
*pData = (char *)*pData + copystartpos;
bret = true;
}
}
}
*pbPostProcessed = data->GetPostProcessed();
// Release lock at the end of mixing
QueueUnlock( handle );
return bret;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : handle -
// *filename -
// datasize -
// startpos -
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::IsDataLoadCompleted( memhandle_t handle, bool *pIsValid )
{
VPROF( "CAsyncWavDataCache::IsDataLoadCompleted" );
CAsyncWaveData *data = CacheGet( handle );
if ( !data )
{
*pIsValid = false;
return false;
}
*pIsValid = true;
// bump the priority
data->SetAsyncPriority( 1 );
return data->m_bLoaded;
}
void CAsyncWavDataCache::RestartDataLoad( memhandle_t *pHandle, const char *pFilename, int dataSize, int startpos )
{
CAsyncWaveData *data = CacheGet( *pHandle );
if ( !data )
{
*pHandle = AsyncLoadCache( pFilename, dataSize, startpos );
}
}
bool CAsyncWavDataCache::IsDataLoadInProgress( memhandle_t handle )
{
CAsyncWaveData *data = CacheGet( handle );
if ( data )
{
return data->IsCurrentlyLoading();
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::Flush()
{
GetCacheSection()->Flush();
SpewMemoryUsage( 0 );
}
void CAsyncWavDataCache::QueueUnlock( const memhandle_t &handle )
{
// not queuing right now, just unlock
if ( !m_bQueueCacheUnlocks )
{
CacheUnlock( handle );
return;
}
// queue to unlock at the end of mixing
m_unlockQueue.AddToTail( handle );
}
void CAsyncWavDataCache::OnMixBegin()
{
Assert( !m_bQueueCacheUnlocks );
m_bQueueCacheUnlocks = true;
Assert( m_unlockQueue.Count() == 0 );
}
void CAsyncWavDataCache::OnMixEnd()
{
m_bQueueCacheUnlocks = false;
// flush the unlock queue
for ( int i = 0; i < m_unlockQueue.Count(); i++ )
{
CacheUnlock( m_unlockQueue[i] );
}
m_unlockQueue.RemoveAll();
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
bool CAsyncWavDataCache::GetItemName( DataCacheClientID_t clientId, const void *pItem, char *pDest, unsigned nMaxLen )
{
CAsyncWaveData *pWaveData = (CAsyncWaveData *)pItem;
Q_strncpy( pDest, pWaveData->GetFileName(), nMaxLen );
return true;
}
//-----------------------------------------------------------------------------
// Purpose: Spew a cache summary to the console
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::SpewMemoryUsage( int level )
{
DataCacheStatus_t status;
DataCacheLimits_t limits;
GetCacheSection()->GetStatus( &status, &limits );
int bytesUsed = status.nBytes;
int bytesTotal = limits.nMaxBytes;
if ( IsPC() )
{
float percent = 100.0f * (float)bytesUsed / (float)bytesTotal;
Msg( "CAsyncWavDataCache: %i .wavs total %s, %.2f %% of capacity\n", m_CacheHandles.Count(), Q_pretifymem( bytesUsed, 2 ), percent );
if ( level >= 1 )
{
for ( int i = m_CacheHandles.FirstInorder(); m_CacheHandles.IsValidIndex(i); i = m_CacheHandles.NextInorder(i) )
{
char name[MAX_PATH];
if ( !g_pFileSystem->String( m_CacheHandles[ i ].name, name, sizeof( name ) ) )
{
Assert( 0 );
continue;
}
memhandle_t &handle = m_CacheHandles[ i ].handle;
CAsyncWaveData *data = CacheGetNoTouch( handle );
if ( data )
{
Msg( "\t%16.16s : %s\n", Q_pretifymem(data->Size()),name);
}
else
{
Msg( "\t%16.16s : %s\n", "not resident",name);
}
}
Msg( "CAsyncWavDataCache: %i .wavs total %s, %.2f %% of capacity\n", m_CacheHandles.Count(), Q_pretifymem( bytesUsed, 2 ), percent );
}
}
if ( IsX360() )
{
CAsyncWaveData *pData;
BufferEntry_t *pBuffer;
BufferHandle_t h;
float percent;
int lockCount;
if ( bytesTotal <= 0 )
{
// unbounded, indeterminate
percent = 0;
bytesTotal = 0;
}
else
{
percent = 100.0f*(float)bytesUsed/(float)bytesTotal;
}
if ( level >= 1 )
{
// detail buffers
ConMsg( "Streaming Buffer List:\n" );
for ( h = m_BufferList.FirstInorder(); h != m_BufferList.InvalidIndex(); h = m_BufferList.NextInorder( h ) )
{
pBuffer = &m_BufferList[h];
pData = CacheGetNoTouch( pBuffer->m_hWaveData );
lockCount = GetCacheSection()->GetLockCount( pBuffer->m_hWaveData );
CacheLockMutex();
if ( pData )
{
ConMsg( "Start:%7d Length:%7d Lock:%3d %s\n", pData->m_async.nOffset, pData->m_nDataSize, lockCount, pData->GetFileName() );
}
CacheUnlockMutex();
}
}
ConMsg( "CAsyncWavDataCache: %.2f MB used of %.2f MB, %.2f%% of capacity", (float)bytesUsed/(1024.0f*1024.0f), (float)bytesTotal/(1024.0f*1024.0f), percent );
}
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
void CAsyncWavDataCache::Clear()
{
for ( int i = m_CacheHandles.FirstInorder(); m_CacheHandles.IsValidIndex(i); i = m_CacheHandles.NextInorder(i) )
{
CacheEntry_t& dat = m_CacheHandles[i];
CacheRemove( dat.handle );
}
m_CacheHandles.RemoveAll();
FOR_EACH_LL( m_StreamedHandles, i )
{
StreamedEntry_t &dat = m_StreamedHandles[i];
for ( int j=0; j<dat.m_numBuffers; ++j )
{
GetCacheSection()->BreakLock( dat.m_hWaveData[j] );
CacheRemove( dat.m_hWaveData[j] );
}
}
m_StreamedHandles.RemoveAll();
m_BufferList.RemoveAll();
}
static CAsyncWavDataCache g_AsyncWaveDataCache;
IAsyncWavDataCache *wavedatacache = &g_AsyncWaveDataCache;
CON_COMMAND( snd_async_flush, "Flush all unlocked async audio data" )
{
g_AsyncWaveDataCache.Flush();
}
CON_COMMAND( snd_async_showmem, "Show async memory stats" )
{
g_AsyncWaveDataCache.SpewMemoryUsage( 1 );
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : *pFileName -
// dataOffset -
// dataSize -
//-----------------------------------------------------------------------------
void PrefetchDataStream( const char *pFileName, int dataOffset, int dataSize )
{
if ( IsX360() )
{
// Xbox streaming buffer implementation does not support this "hinting"
return;
}
wavedatacache->PrefetchCache( pFileName, dataSize, dataOffset );
}
//-----------------------------------------------------------------------------
// Purpose: This is an instance of a stream.
// This contains the file handle and streaming buffer
// The mixer doesn't know the file is streaming. The IWaveData
// abstracts the data access. The mixer abstracts data encoding/format
//-----------------------------------------------------------------------------
class CWaveDataStreamAsync : public IWaveData
{
public:
CWaveDataStreamAsync( CAudioSource &source, IWaveStreamSource *pStreamSource, const char *pFileName, int fileStart, int fileSize, CSfxTable *sfx, int startOffset );
~CWaveDataStreamAsync( void );
// return the source pointer (mixer needs this to determine some things like sampling rate)
CAudioSource &Source( void ) { return m_source; }
// Read data from the source - this is the primary function of a IWaveData subclass
// Get the data from the buffer (or reload from disk)
virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] );
bool IsValid() { return m_bValid; }
virtual bool IsReadyToMix();
private:
CWaveDataStreamAsync( const CWaveDataStreamAsync & );
//-----------------------------------------------------------------------------
// Purpose:
// Output : byte
//-----------------------------------------------------------------------------
inline byte *GetCachedDataPointer()
{
VPROF( "CWaveDataStreamAsync::GetCachedDataPointer" );
CAudioSourceCachedInfo *info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
if ( !info )
{
Assert( !"CAudioSourceWave::GetCachedDataPointer info == NULL" );
return NULL;
}
return (byte *)info->CachedData();
}
char const *GetFileName();
CAudioSource &m_source; // wave source
IWaveStreamSource *m_pStreamSource; // streaming
int m_sampleSize; // size of a sample in bytes
int m_waveSize; // total number of samples in the file
int m_bufferSize; // size of buffer in samples
char *m_buffer;
int m_sampleIndex;
int m_bufferCount;
int m_dataStart;
int m_dataSize;
memhandle_t m_hCache;
StreamHandle_t m_hStream;
FileNameHandle_t m_hFileName;
bool m_bValid;
CAudioSourceCachedInfoHandle_t m_AudioCacheHandle;
int m_nCachedDataSize;
CSfxTable *m_pSfx;
};
CWaveDataStreamAsync::CWaveDataStreamAsync
(
CAudioSource &source,
IWaveStreamSource *pStreamSource,
const char *pFileName,
int fileStart,
int fileSize,
CSfxTable *sfx,
int startOffset
) :
m_source( source ),
m_dataStart( fileStart ),
m_dataSize( fileSize ),
m_pStreamSource( pStreamSource ),
m_bValid( false ),
m_hCache( 0 ),
m_hStream( INVALID_STREAM_HANDLE ),
m_hFileName( 0 ),
m_pSfx( sfx )
{
m_hFileName = g_pFileSystem->FindOrAddFileName( pFileName );
// nothing in the buffer yet
m_sampleIndex = 0;
m_bufferCount = 0;
if ( IsPC() )
{
m_buffer = new char[SINGLE_BUFFER_SIZE];
Q_memset( m_buffer, 0, SINGLE_BUFFER_SIZE );
}
m_nCachedDataSize = 0;
if ( m_dataSize <= 0 )
{
DevMsg(1, "Can't find streaming wav file: sound\\%s\n", GetFileName() );
return;
}
if ( IsPC() )
{
m_hCache = wavedatacache->AsyncLoadCache( GetFileName(), m_dataSize, m_dataStart );
// size of a sample
m_sampleSize = source.SampleSize();
// size in samples of the buffer
m_bufferSize = SINGLE_BUFFER_SIZE / m_sampleSize;
// size in samples (not bytes) of the wave itself
m_waveSize = fileSize / m_sampleSize;
m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
}
if ( IsX360() )
{
// size of a sample
m_sampleSize = source.SampleSize();
// size in samples (not bytes) of the wave itself
m_waveSize = fileSize / m_sampleSize;
streamFlags_t flags = STREAMED_FROMDVD;
if ( !Q_strnicmp( pFileName, "music", 5 ) && ( pFileName[5] == '\\' || pFileName[5] == '/') )
{
// music discards and cycles its buffers
flags |= STREAMED_SINGLEPLAY;
}
else if ( !Q_strnicmp( pFileName, "vo", 2 ) && ( pFileName[2] == '\\' || pFileName[2] == '/' ) && !source.IsSentenceWord() )
{
// vo discards and cycles its buffers, except for sentence sources, which do recur
flags |= STREAMED_SINGLEPLAY;
}
int bufferSize;
if ( source.Format() == WAVE_FORMAT_XMA )
{
// each xma block has its own compression rate
// the buffer must be large enough to cover worst case delivery i/o latency
// the xma mixer expects quantum xma blocks
COMPILE_TIME_ASSERT( ( STREAM_BUFFER_DATASIZE % XMA_BLOCK_SIZE ) == 0 );
bufferSize = STREAM_BUFFER_DATASIZE;
}
else
{
// calculate a worst case buffer size based on rate
bufferSize = STREAM_BUFFER_TIME*source.SampleRate()*m_sampleSize;
if ( source.Format() == WAVE_FORMAT_ADPCM )
{
// consider adpcm as 4 bit samples
bufferSize /= 2;
}
if ( source.IsLooped() )
{
// lighten the streaming load for looping samples
// doubling the buffer halves the buffer search/load requests
bufferSize *= 2;
}
}
// streaming buffers obey alignments
bufferSize = AlignValue( bufferSize, XBOX_DVD_SECTORSIZE );
// use double buffering
int numBuffers = 2;
if ( m_dataSize <= STREAM_BUFFER_DATASIZE || m_dataSize <= numBuffers*bufferSize )
{
// no gain for buffering a small file or multiple buffering
// match the expected transfer with a single buffer
bufferSize = m_dataSize;
numBuffers = 1;
}
// size in samples of the transfer buffer
m_bufferSize = bufferSize / m_sampleSize;
// allocate a transfer buffer
// matches the size of the streaming buffer exactly
// ensures that buffers can be filled and then consumed/requeued at the same time
m_buffer = new char[bufferSize];
int loopStart;
if ( source.IsLooped() )
{
int loopBlock;
loopStart = m_pStreamSource->GetLoopingInfo( &loopBlock, NULL, NULL ) * m_sampleSize;
if ( source.Format() == WAVE_FORMAT_XMA )
{
// xma works in blocks, mixer handles inter-block accurate loop positioning
// block streaming will cycle from the block where the loop occurs
loopStart = loopBlock * XMA_BLOCK_SIZE;
}
}
else
{
// sample not looped
loopStart = -1;
}
// load the file piecewise through a buffering implementation
m_hStream = wavedatacache->OpenStreamedLoad( pFileName, m_dataSize, m_dataStart, startOffset, loopStart, bufferSize, numBuffers, flags );
}
m_bValid = true;
}
//-----------------------------------------------------------------------------
// Purpose:
//-----------------------------------------------------------------------------
CWaveDataStreamAsync::~CWaveDataStreamAsync( void )
{
if ( IsPC() && m_source.IsPlayOnce() && m_source.CanDelete() )
{
m_source.SetPlayOnce( false ); // in case it gets used again
wavedatacache->Unload( m_hCache );
}
if ( IsX360() )
{
wavedatacache->CloseStreamedLoad( m_hStream );
}
delete [] m_buffer;
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : char const
//-----------------------------------------------------------------------------
char const *CWaveDataStreamAsync::GetFileName()
{
static char fn[MAX_PATH];
if ( m_hFileName )
{
if ( g_pFileSystem->String( m_hFileName, fn, sizeof( fn ) ) )
{
return fn;
}
}
Assert( 0 );
return "";
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CWaveDataStreamAsync::IsReadyToMix()
{
if ( IsPC() )
{
// If not async loaded, start mixing right away
if ( !m_source.IsAsyncLoad() && !snd_async_fullyasync.GetBool() )
{
return true;
}
bool bCacheValid;
bool bLoaded = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid );
if ( !bCacheValid )
{
wavedatacache->RestartDataLoad( &m_hCache, GetFileName(), m_dataSize, m_dataStart );
}
return bLoaded;
}
if ( IsX360() )
{
return wavedatacache->IsStreamedDataReady( m_hStream );
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose: Read data from the source - this is the primary function of a IWaveData subclass
// Get the data from the buffer (or reload from disk)
// Input : **pData -
// sampleIndex -
// sampleCount -
// copyBuf[AUDIOSOURCE_COPYBUF_SIZE] -
// Output : int
//-----------------------------------------------------------------------------
int CWaveDataStreamAsync::ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
{
// Current file position
int seekpos = m_dataStart + m_sampleIndex * m_sampleSize;
// wrap position if looping
if ( m_source.IsLooped() )
{
sampleIndex = m_pStreamSource->UpdateLoopingSamplePosition( sampleIndex );
if ( sampleIndex < m_sampleIndex )
{
// looped back, buffer has no samples yet
m_sampleIndex = sampleIndex;
m_bufferCount = 0;
// update file position
seekpos = m_dataStart + sampleIndex * m_sampleSize;
}
}
// UNDONE: This is an error!!
// The mixer playing back the stream tried to go backwards!?!?!
// BUGBUG: Just play the beginning of the buffer until we get to a valid linear position
if ( sampleIndex < m_sampleIndex )
sampleIndex = m_sampleIndex;
// calc sample position relative to the current buffer
// m_sampleIndex is the sample position of the first byte of the buffer
sampleIndex -= m_sampleIndex;
// out of range? refresh buffer
if ( sampleIndex >= m_bufferCount )
{
// advance one buffer (the file is positioned here)
m_sampleIndex += m_bufferCount;
// next sample to load
sampleIndex -= m_bufferCount;
// if the remainder is greated than one buffer size, seek over it. Otherwise, read the next chunk
// and leave the remainder as an offset.
// number of buffers to "skip" (as in the case where we are starting a streaming sound not at the beginning)
int skips = sampleIndex / m_bufferSize;
// If we are skipping over a buffer, do it with a seek instead of a read.
if ( skips )
{
// skip directly to next position
m_sampleIndex += sampleIndex;
sampleIndex = 0;
}
// move the file to the new position
seekpos = m_dataStart + (m_sampleIndex * m_sampleSize);
// This is the maximum number of samples we could read from the file
m_bufferCount = m_waveSize - m_sampleIndex;
// past the end of the file? stop the wave.
if ( m_bufferCount <= 0 )
return 0;
// clamp available samples to buffer size
if ( m_bufferCount > m_bufferSize )
m_bufferCount = m_bufferSize;
if ( IsPC() )
{
// See if we can load in the intial data right out of the cached data lump instead.
int cacheddatastartpos = ( seekpos - m_dataStart );
// FastGet doesn't call into IsPrecachedSound if the handle appears valid...
CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet();
if ( !info )
{
// Full recache
info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
}
bool startupCacheUsed = false;
if ( info &&
( m_nCachedDataSize > 0 ) &&
( cacheddatastartpos < m_nCachedDataSize ) )
{
// Get a ptr to the cached data
const byte *cacheddata = info->CachedData();
if ( cacheddata )
{
// See how many samples of cached data are available (cacheddatastartpos is zero on the first read)
int availSamples = ( m_nCachedDataSize - cacheddatastartpos ) / m_sampleSize;
// Clamp to size of our internal buffer
if ( availSamples > m_bufferSize )
{
availSamples = m_bufferSize;
}
// Mark how many we are returning
m_bufferCount = availSamples;
// Copy raw sample data directly out of cache
Q_memcpy( m_buffer, ( char * )cacheddata + cacheddatastartpos, availSamples * m_sampleSize );
startupCacheUsed = true;
}
}
// Not in startup cache, grab data from async cache loader (will block if data hasn't arrived yet)
if ( !startupCacheUsed )
{
bool postprocessed = false;
// read in the max bufferable, available samples
if ( !wavedatacache->CopyDataIntoMemory(
m_hCache,
GetFileName(),
m_dataSize,
m_dataStart,
m_buffer,
sizeof( m_buffer ),
seekpos,
m_bufferCount * m_sampleSize,
&postprocessed ) )
{
return 0;
}
// do any conversion the source needs (mixer will decode/decompress)
if ( !postprocessed )
{
// Note that we don't set the postprocessed flag on the underlying data, since for streaming we're copying the
// original data into this buffer instead.
m_pStreamSource->UpdateSamples( m_buffer, m_bufferCount );
}
}
}
if ( IsX360() )
{
if ( m_hStream != INVALID_STREAM_HANDLE )
{
// request available data, may get less
// drives the buffering
m_bufferCount = wavedatacache->CopyStreamedDataIntoMemory(
m_hStream,
m_buffer,
m_bufferSize * m_sampleSize,
seekpos,
m_bufferCount * m_sampleSize );
// convert to number of samples in the buffer
m_bufferCount /= m_sampleSize;
}
else
{
return 0;
}
// do any conversion now the source needs (mixer will decode/decompress) on this buffer
m_pStreamSource->UpdateSamples( m_buffer, m_bufferCount );
}
}
// If we have some samples in the buffer that are within range of the request
// Use unsigned comparisons so that if sampleIndex is somehow negative that
// will be treated as out of range.
if ( (unsigned)sampleIndex < (unsigned)m_bufferCount )
{
// Get the desired starting sample
*pData = (void *)&m_buffer[sampleIndex * m_sampleSize];
// max available
int available = m_bufferCount - sampleIndex;
// clamp available to max requested
if ( available > sampleCount )
available = sampleCount;
return available;
}
return 0;
}
//-----------------------------------------------------------------------------
// Purpose: Iterator for wave data (this is to abstract streaming/buffering)
//-----------------------------------------------------------------------------
class CWaveDataMemoryAsync : public IWaveData
{
public:
CWaveDataMemoryAsync( CAudioSource &source );
~CWaveDataMemoryAsync( void ) {}
CAudioSource &Source( void ) { return m_source; }
virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] );
virtual bool IsReadyToMix();
private:
CAudioSource &m_source; // pointer to source
};
//-----------------------------------------------------------------------------
// Purpose:
// Input : &source -
//-----------------------------------------------------------------------------
CWaveDataMemoryAsync::CWaveDataMemoryAsync( CAudioSource &source ) :
m_source(source)
{
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : **pData -
// sampleIndex -
// sampleCount -
// copyBuf[AUDIOSOURCE_COPYBUF_SIZE] -
// Output : int
//-----------------------------------------------------------------------------
int CWaveDataMemoryAsync::ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
{
return m_source.GetOutputData( pData, sampleIndex, sampleCount, copyBuf );
}
//-----------------------------------------------------------------------------
// Purpose:
// Output : Returns true on success, false on failure.
//-----------------------------------------------------------------------------
bool CWaveDataMemoryAsync::IsReadyToMix()
{
if ( !m_source.IsAsyncLoad() && !snd_async_fullyasync.GetBool() )
{
// Wait until we're pending at least
if ( m_source.GetCacheStatus() == CAudioSource::AUDIO_NOT_LOADED )
{
return false;
}
return true;
}
if ( m_source.IsCached() )
{
return true;
}
if ( IsPC() )
{
// Msg( "Waiting for data '%s'\n", m_source.GetFileName() );
m_source.CacheLoad();
}
if ( IsX360() )
{
// expected to be resident and valid, otherwise being called prior to load
Assert( 0 );
}
return false;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &source -
// *pStreamSource -
// &io -
// *pFileName -
// dataOffset -
// dataSize -
// Output : IWaveData
//-----------------------------------------------------------------------------
IWaveData *CreateWaveDataStream( CAudioSource &source, IWaveStreamSource *pStreamSource, const char *pFileName, int dataStart, int dataSize, CSfxTable *pSfx, int startOffset )
{
CWaveDataStreamAsync *pStream = new CWaveDataStreamAsync( source, pStreamSource, pFileName, dataStart, dataSize, pSfx, startOffset );
if ( !pStream || !pStream->IsValid() )
{
delete pStream;
pStream = NULL;
}
return pStream;
}
//-----------------------------------------------------------------------------
// Purpose:
// Input : &source -
// Output : IWaveData
//-----------------------------------------------------------------------------
IWaveData *CreateWaveDataMemory( CAudioSource &source )
{
CWaveDataMemoryAsync *mem = new CWaveDataMemoryAsync( source );
return mem;
}