//========= 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 ¶ms ); static unsigned int EstimatedSize( const asyncwaveparams_t ¶ms ); 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 : ¶ms - // Output : CAsyncWaveData //----------------------------------------------------------------------------- CAsyncWaveData *CAsyncWaveData::CreateResource( const asyncwaveparams_t ¶ms ) { 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 : ¶ms - // Output : static unsigned int //----------------------------------------------------------------------------- unsigned int CAsyncWaveData::EstimatedSize( const asyncwaveparams_t ¶ms ) { 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 { 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 ¶ms, bool bFind ); bool m_bInitialized; bool m_bQueueCacheUnlocks; CUtlVector 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 ¶ms, 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; iGetLockCount( 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; im_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; jBreakLock( 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; }