You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3487 lines
90 KiB
3487 lines
90 KiB
//========= Copyright � 1996-2005, Valve Corporation, All rights reserved. ============//
|
|
//
|
|
// Purpose:
|
|
//
|
|
// $NoKeywords: $
|
|
//
|
|
//=============================================================================//
|
|
|
|
#include "audio_pch.h"
|
|
#include "snd_mp3_source.h"
|
|
#include "utlsymbol.h"
|
|
#include "checksum_crc.h"
|
|
#include "../../host.h"
|
|
#include "xwvfile.h"
|
|
#include "filesystem/IQueuedLoader.h"
|
|
#include "tier1/lzmaDecoder.h"
|
|
#include "tier1/fmtstr.h"
|
|
#include "characterset.h"
|
|
|
|
// memdbgon must be the last include file in a .cpp file!!!
|
|
#include "tier0/memdbgon.h"
|
|
|
|
// This determines how much data to pre-cache (will invalidate per-map caches if changed).
|
|
#define SOUND_DIRECTORY_LENGTH 6 // i.e., Q_strlen( "sound/" )
|
|
#define MASTER_CACHE_NAME "_master"
|
|
#define SOUND_PREFETCH_FILE "scripts/sound_prefetch.txt"
|
|
|
|
extern ConVar snd_async_spew_blocking;
|
|
extern double realtime;
|
|
ConVar snd_async_minsize("snd_async_minsize", "262144");
|
|
static ConVar snd_prefetch_common( "snd_prefetch_common", "1", FCVAR_RELEASE, "Prefetch common sounds from directories specified in " SOUND_PREFETCH_FILE );
|
|
|
|
ConVar force_audio_english( "force_audio_english", "0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_GAMECONSOLE, "Keeps track of whether we're forcing english in a localized language." );
|
|
|
|
// #define DEBUG_CHUNKS
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Report chunk error
|
|
// Input : id - chunk FOURCC
|
|
//-----------------------------------------------------------------------------
|
|
void ChunkError( unsigned int id )
|
|
{
|
|
#if defined( DEBUG_CHUNKS ) && defined( _DEBUG )
|
|
if ( id == WAVE_LIST || id == WAVE_FACT )
|
|
{
|
|
// unused chunks, not an error
|
|
return;
|
|
}
|
|
|
|
char tmp[256];
|
|
char idname[5];
|
|
idname[4] = 0;
|
|
memcpy( idname, &id, 4 );
|
|
|
|
Q_snprintf( tmp, sizeof( tmp ), "Unhandled chunk %s\n", idname );
|
|
Plat_DebugString( tmp );
|
|
#endif
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Determine a true sample count for an ADPCM blob
|
|
//-----------------------------------------------------------------------------
|
|
int ADPCMSampleCount( ADPCMWAVEFORMAT *pFormat, int length )
|
|
{
|
|
// determine a true sample count
|
|
int nChannels = LittleWord( pFormat->wfx.nChannels );
|
|
int wSamplesPerBlock = LittleWord( pFormat->wSamplesPerBlock );
|
|
|
|
int blockSize = (( wSamplesPerBlock - 2) * nChannels ) / 2;
|
|
blockSize += 7 * nChannels;
|
|
|
|
int blockCount = length / blockSize;
|
|
int blockRem = length % blockSize;
|
|
|
|
// total samples in complete blocks
|
|
int sampleCount = blockCount * wSamplesPerBlock;
|
|
|
|
// add remaining in a short block
|
|
if ( blockRem )
|
|
{
|
|
sampleCount += wSamplesPerBlock - (((blockSize - blockRem) * 2) / nChannels );
|
|
}
|
|
|
|
return sampleCount;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Init to empty wave
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceWave::CAudioSourceWave( CSfxTable *pSfx )
|
|
{
|
|
m_format = 0;
|
|
m_pHeader = NULL;
|
|
m_nHeaderSize = 0;
|
|
|
|
// no looping
|
|
m_loopStart = -1;
|
|
|
|
m_sampleSize = 1;
|
|
m_sampleCount = 0;
|
|
m_bits = 0;
|
|
m_channels = 0;
|
|
m_dataStart = 0;
|
|
m_dataSize = 0;
|
|
m_rate = 0;
|
|
|
|
m_refCount = 0;
|
|
|
|
m_pSfx = pSfx;
|
|
#ifdef _DEBUG
|
|
if ( m_pSfx )
|
|
{
|
|
char buf[MAX_PATH];
|
|
m_pDebugName = strdup( m_pSfx->getname(buf, sizeof(buf)) );
|
|
}
|
|
#endif
|
|
|
|
m_bNoSentence = false;
|
|
m_pTempSentence = NULL;
|
|
m_nCachedDataSize = 0;
|
|
m_bIsPlayOnce = false;
|
|
m_bIsSentenceWord = false;
|
|
|
|
m_numDecodedSamples = 0;
|
|
|
|
// TERROR: limit data read while rebuilding cache
|
|
m_bIsRebuildingCache = false;
|
|
}
|
|
|
|
CAudioSourceWave::CAudioSourceWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info )
|
|
{
|
|
m_pSfx = pSfx;
|
|
#ifdef _DEBUG
|
|
if ( m_pSfx )
|
|
{
|
|
char buf[MAX_PATH];
|
|
m_pDebugName = strdup( m_pSfx->getname(buf, sizeof(buf)) );
|
|
}
|
|
#endif
|
|
|
|
m_refCount = 0;
|
|
|
|
m_pHeader = NULL;
|
|
m_nHeaderSize = 0;
|
|
|
|
if ( info->HeaderData() )
|
|
{
|
|
m_pHeader = new char[ info->HeaderSize() ];
|
|
Assert( m_pHeader );
|
|
Q_memcpy( m_pHeader, info->HeaderData(), info->HeaderSize() );
|
|
m_nHeaderSize = info->HeaderSize();
|
|
}
|
|
|
|
m_bits = info->Bits();
|
|
m_channels = info->Channels();
|
|
m_sampleSize = info->SampleSize();
|
|
m_format = info->Format();
|
|
m_dataStart = info->DataStart();
|
|
m_dataSize = info->DataSize();
|
|
m_rate = info->SampleRate();
|
|
m_loopStart = info->LoopStart();
|
|
m_sampleCount = info->SampleCount();
|
|
m_numDecodedSamples = m_sampleCount;
|
|
|
|
if ( m_format == WAVE_FORMAT_ADPCM && m_pHeader )
|
|
{
|
|
m_numDecodedSamples = ADPCMSampleCount( (ADPCMWAVEFORMAT *)m_pHeader, m_sampleCount );
|
|
}
|
|
|
|
m_bNoSentence = false;
|
|
m_pTempSentence = NULL;
|
|
m_nCachedDataSize = 0;
|
|
m_bIsPlayOnce = false;
|
|
m_bIsSentenceWord = false;
|
|
|
|
// TERROR: limit data read while rebuilding cache
|
|
m_bIsRebuildingCache = false;
|
|
}
|
|
|
|
|
|
CAudioSourceWave::~CAudioSourceWave( void )
|
|
{
|
|
#if _DEBUG
|
|
if ( !CanDelete() )
|
|
Assert(0);
|
|
#endif
|
|
|
|
// for non-standard waves, we store a copy of the header in RAM
|
|
delete[] m_pHeader;
|
|
delete m_pTempSentence;
|
|
}
|
|
|
|
int CAudioSourceWave::GetType( void )
|
|
{
|
|
return AUDIO_SOURCE_WAV;
|
|
}
|
|
|
|
void CAudioSourceWave::GetCacheData( CAudioSourceCachedInfo *info )
|
|
{
|
|
Assert( info->Type() == CAudioSource::AUDIO_SOURCE_WAV );
|
|
|
|
int nFileSize;
|
|
if (GetStartupData( nFileSize ) == false)
|
|
{
|
|
m_dataStart = 0;
|
|
m_dataSize = nFileSize;
|
|
|
|
// START OF HACK - HACK - HACK! Necessary for Portal 2 so it fits in one single DVD
|
|
// If that's not a WAV file, assume that's an MP3
|
|
// Unfortunately shortcomings of the sound cache force us to do this here
|
|
info->SetBits( 8 );
|
|
info->SetChannels( 1 ); // Make this more accurate at some point
|
|
info->SetSampleRate( 44100 ); // Make this more accurate at some point
|
|
info->SetLoopStart( -1 ); // No loop
|
|
info->SetSampleCount( m_dataSize );
|
|
info->SetDataSize( m_dataSize );
|
|
info->SetDataStart( 0 );
|
|
info->SetType( CAudioSource::AUDIO_SOURCE_MP3 );
|
|
// END OF HACK - HACK - HACK!
|
|
return;
|
|
}
|
|
|
|
info->SetBits( m_bits );
|
|
info->SetChannels( m_channels );
|
|
info->SetSampleSize( m_sampleSize );
|
|
info->SetFormat( m_format );
|
|
info->SetDataStart( m_dataStart ); // offset of wave data chunk
|
|
info->SetDataSize( m_dataSize ); // size of wave data chunk
|
|
info->SetSampleRate( m_rate );
|
|
info->SetLoopStart( m_loopStart );
|
|
info->SetSampleCount( m_sampleCount );
|
|
|
|
if ( m_pTempSentence )
|
|
{
|
|
CSentence *scopy = new CSentence;
|
|
*scopy = *m_pTempSentence;
|
|
info->SetSentence( scopy );
|
|
|
|
// Wipe it down to basically nothing
|
|
delete m_pTempSentence;
|
|
m_pTempSentence = NULL;
|
|
}
|
|
|
|
if ( m_pHeader && m_nHeaderSize > 0 )
|
|
{
|
|
byte *data = new byte[ m_nHeaderSize ];
|
|
Q_memcpy( data, m_pHeader, m_nHeaderSize );
|
|
info->SetHeaderSize( m_nHeaderSize );
|
|
info->SetHeaderData( data );
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : char const
|
|
//-----------------------------------------------------------------------------
|
|
char const *CAudioSourceWave::GetFileName( char *pOutBuf, size_t bufLen )
|
|
{
|
|
return m_pSfx ? m_pSfx->GetFileName( pOutBuf, bufLen ) : "NULL m_pSfx";
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceWave::IsAsyncLoad()
|
|
{
|
|
VPROF("CAudioSourceWave::IsAsyncLoad");
|
|
|
|
if ( ( IsPC() || !IsGameConsole() ) && !m_AudioCacheHandle.IsValid() )
|
|
{
|
|
m_AudioCacheHandle.Get( GetType(), m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
|
|
}
|
|
|
|
// If there's a bit of "cached data" then we don't have to lazy/async load (we still async load the remaining data,
|
|
// but we run from the cache initially)
|
|
if ( m_dataSize > snd_async_minsize.GetInt() )
|
|
return true;
|
|
return ( m_nCachedDataSize > 0 ) ? false : true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::CheckAudioSourceCache()
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 does not use audio cache files
|
|
return;
|
|
}
|
|
|
|
Assert( m_pSfx );
|
|
|
|
if ( !m_pSfx->IsPrecachedSound() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// This will "re-cache" this if it's not in this level's cache already
|
|
m_AudioCacheHandle.Get( GetType(), true, m_pSfx, &m_nCachedDataSize );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Init the wave data.
|
|
// Input : *pHeaderBuffer - the RIFF fmt chunk
|
|
// headerSize - size of that chunk
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::Init( const char *pHeaderBuffer, int headerSize )
|
|
{
|
|
const WAVEFORMATEX *pHeader = (const WAVEFORMATEX *)pHeaderBuffer;
|
|
|
|
// copy the relevant header data
|
|
m_format = LittleWord( pHeader->wFormatTag );
|
|
m_bits = LittleWord( pHeader->wBitsPerSample );
|
|
m_rate = LittleDWord( pHeader->nSamplesPerSec );
|
|
m_channels = LittleWord( pHeader->nChannels );
|
|
m_sampleSize = (m_bits * m_channels)/8;
|
|
|
|
// this can never be zero -- other functions divide by this.
|
|
// this should never happen, but avoid crashing
|
|
if ( m_sampleSize <= 0 )
|
|
{
|
|
m_sampleSize = 1;
|
|
}
|
|
|
|
if ( m_format == WAVE_FORMAT_ADPCM )
|
|
{
|
|
// For non-standard waves (like ADPCM) store the header, it has the decoding coefficients
|
|
m_pHeader = new char[headerSize];
|
|
memcpy( m_pHeader, pHeader, headerSize );
|
|
m_nHeaderSize = headerSize;
|
|
|
|
// treat ADPCM sources as a file of bytes. They are decoded by the mixer
|
|
m_sampleSize = 1;
|
|
}
|
|
}
|
|
|
|
|
|
int CAudioSourceWave::SampleRate( void )
|
|
{
|
|
return m_rate;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Size of each sample
|
|
// Output :
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceWave::SampleSize( void )
|
|
{
|
|
return m_sampleSize;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Total number of samples in this source
|
|
// Output : int
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceWave::SampleCount( void )
|
|
{
|
|
// caller wants real samples
|
|
return m_numDecodedSamples;
|
|
}
|
|
|
|
int CAudioSourceWave::Format( void )
|
|
{
|
|
return m_format;
|
|
}
|
|
|
|
int CAudioSourceWave::DataSize( void )
|
|
{
|
|
return m_dataSize;
|
|
}
|
|
|
|
bool CAudioSourceWave::IsVoiceSource()
|
|
{
|
|
if ( GetSentence() )
|
|
{
|
|
if ( GetSentence()->GetVoiceDuck() )
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Do any sample conversion
|
|
// For 8 bit PCM, convert to signed because the mixing routine assumes this
|
|
// Input : *pData - pointer to sample data
|
|
// sampleCount - number of samples
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ConvertSamples( char *pData, int sampleCount )
|
|
{
|
|
if ( m_format == WAVE_FORMAT_PCM )
|
|
{
|
|
if ( m_bits == 8 )
|
|
{
|
|
for ( int i = 0; i < sampleCount*m_channels; i++ )
|
|
{
|
|
*pData = (unsigned char)((int)((unsigned)*pData) - 128);
|
|
pData++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Parse base chunks
|
|
// Input : &walk - riff file to parse
|
|
// : chunkName - name of the chunk to parse
|
|
//-----------------------------------------------------------------------------
|
|
// UNDONE: Move parsing loop here and drop each chunk into a virtual function
|
|
// instead of this being virtual.
|
|
void CAudioSourceWave::ParseChunk( IterateRIFF &walk, int chunkName )
|
|
{
|
|
switch( chunkName )
|
|
{
|
|
case WAVE_CUE:
|
|
ParseCueChunk( walk );
|
|
break;
|
|
case WAVE_SAMPLER:
|
|
ParseSamplerChunk( walk );
|
|
break;
|
|
case WAVE_VALVEDATA:
|
|
ParseSentence( walk );
|
|
break;
|
|
default:
|
|
// unknown and don't care
|
|
ChunkError( walk.ChunkName() );
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool CAudioSourceWave::IsLooped( void )
|
|
{
|
|
return (m_loopStart >= 0) ? true : false;
|
|
}
|
|
|
|
bool CAudioSourceWave::IsStereoWav( void )
|
|
{
|
|
return (m_channels == 2) ? true : false;
|
|
}
|
|
|
|
bool CAudioSourceWave::IsStreaming( void )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
|
|
int CAudioSourceWave::GetCacheStatus( void )
|
|
{
|
|
return AUDIO_IS_LOADED;
|
|
}
|
|
|
|
void CAudioSourceWave::CacheLoad( void )
|
|
{
|
|
}
|
|
|
|
void CAudioSourceWave::CacheUnload( void )
|
|
{
|
|
}
|
|
|
|
int CAudioSourceWave::ZeroCrossingBefore( int sample )
|
|
{
|
|
return sample;
|
|
}
|
|
|
|
|
|
int CAudioSourceWave::ZeroCrossingAfter( int sample )
|
|
{
|
|
return sample;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : &walk -
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ParseSentence( IterateRIFF &walk )
|
|
{
|
|
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
|
|
|
|
buf.EnsureCapacity( walk.ChunkSize() );
|
|
walk.ChunkRead( buf.Base() );
|
|
buf.SeekPut( CUtlBuffer::SEEK_HEAD, walk.ChunkSize() );
|
|
|
|
m_pTempSentence = new CSentence();
|
|
Assert( m_pTempSentence );
|
|
|
|
m_pTempSentence->InitFromDataChunk( buf.Base(), buf.TellPut() );
|
|
|
|
// Throws all phonemes into one word, discards sentence memory, etc.
|
|
m_pTempSentence->MakeRuntimeOnly();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : CSentence
|
|
//-----------------------------------------------------------------------------
|
|
CSentence *CAudioSourceWave::GetSentence( void )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return m_pTempSentence;
|
|
}
|
|
|
|
// Already checked and this wav doesn't have sentence data...
|
|
if ( m_bNoSentence == true )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
// Look up sentence from cache
|
|
CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet();
|
|
if ( !info )
|
|
{
|
|
info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize );
|
|
}
|
|
Assert( info );
|
|
if ( !info )
|
|
{
|
|
m_bNoSentence = true;
|
|
return NULL;
|
|
}
|
|
|
|
CSentence *sentence = info->Sentence();
|
|
if ( !sentence )
|
|
{
|
|
m_bNoSentence = true;
|
|
return NULL;
|
|
}
|
|
|
|
if ( sentence->m_bIsValid )
|
|
{
|
|
return sentence;
|
|
}
|
|
|
|
m_bNoSentence = true;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
const char *CAudioSourceWave::GetName( char *pBuf, size_t bufLen )
|
|
{
|
|
return m_pSfx ? m_pSfx->getname(pBuf, bufLen) : NULL;
|
|
}
|
|
|
|
int CAudioSourceWave::GetQuality()
|
|
{
|
|
return ( m_format == WAVE_FORMAT_XMA ? m_quality : 0 );
|
|
}
|
|
|
|
PathTypeFilter_t GetAudioPathFilter()
|
|
{
|
|
if ( XBX_IsAudioLocalized() && force_audio_english.GetBool() )
|
|
{
|
|
// skip the localized search paths and fall through to the primary zips
|
|
return FILTER_CULLLOCALIZED;
|
|
}
|
|
|
|
// No audio exists outside of zips, all the audio is inside the zips
|
|
return FILTER_CULLNONPACK;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Load a native xaudio or legacy wav
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceWave::GetXboxAudioStartupData()
|
|
{
|
|
CUtlBuffer buf;
|
|
char fileName[MAX_PATH];
|
|
char tempFileName[MAX_PATH];
|
|
|
|
MEM_ALLOC_CREDIT();
|
|
|
|
// try native optimal xma wav file first
|
|
Q_StripExtension( m_pSfx->GetFileName(fileName, sizeof(fileName)), tempFileName, sizeof( tempFileName ) );
|
|
Q_snprintf( fileName, sizeof( fileName ), "sound\\%s" PLATFORM_EXT ".wav", tempFileName );
|
|
|
|
V_FixDoubleSlashes( fileName );
|
|
PathTypeQuery_t pathType;
|
|
char szFullName[MAX_PATH];
|
|
if ( !g_pFullFileSystem->RelativePathToFullPath( fileName, "GAME", szFullName, sizeof( szFullName ), GetAudioPathFilter(), &pathType ) )
|
|
{
|
|
// not found, not supported
|
|
return false;
|
|
}
|
|
|
|
if ( !g_pFullFileSystem->ReadFile( szFullName, "GAME", buf, sizeof( xwvHeader_t ) ) )
|
|
{
|
|
// not found, not supported
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
xwvHeader_t* pHeader = (xwvHeader_t *)buf.Base();
|
|
if ( pHeader->id != XWV_ID || pHeader->version != XWV_VERSION )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch ( pHeader->format )
|
|
{
|
|
case XWV_FORMAT_XMA:
|
|
m_format = WAVE_FORMAT_XMA;
|
|
break;
|
|
case XWV_FORMAT_PCM:
|
|
m_format = WAVE_FORMAT_PCM;
|
|
break;
|
|
case XWV_FORMAT_MP3:
|
|
m_format = WAVE_FORMAT_MP3;
|
|
break;
|
|
case XWV_FORMAT_TEMP:
|
|
m_format = WAVE_FORMAT_TEMP;
|
|
break;
|
|
default:
|
|
// unknown
|
|
return false;
|
|
}
|
|
|
|
m_rate = pHeader->GetSampleRate();
|
|
m_channels = pHeader->channels;
|
|
m_dataStart = pHeader->dataOffset;
|
|
m_dataSize = pHeader->dataSize;
|
|
m_quality = pHeader->quality;
|
|
|
|
m_loopStart = pHeader->loopStart;
|
|
m_loopBlock = pHeader->loopBlock;
|
|
m_numLeadingSamples = pHeader->numLeadingSamples;
|
|
m_numTrailingSamples = pHeader->numTrailingSamples;
|
|
|
|
switch (m_format)
|
|
{
|
|
case WAVE_FORMAT_MP3:
|
|
case WAVE_FORMAT_TEMP:
|
|
// MP3 and temp format do not store numDecodedSamples correctly (in bytes instead of samples).
|
|
// We would need to change makegamedata accordingly (corresponding code is commented).
|
|
// However I [oliviern] did not want to change the data that late in Portal 2. So patch in the code instead.
|
|
pHeader->numDecodedSamples /= m_channels * sizeof( short );
|
|
|
|
// Pass through...
|
|
|
|
case WAVE_FORMAT_XMA:
|
|
// xma and MP3 are compressed blocks, trick to fool system to treat data as bytes, not samples
|
|
// unfortunate, but callers must know xma context and provide offsets in samples or bytes
|
|
m_bits = 16;
|
|
m_sampleSize = 1;
|
|
m_sampleCount = m_dataSize;
|
|
break;
|
|
|
|
default:
|
|
m_bits = 16;
|
|
m_sampleSize = sizeof( short ) * m_channels;
|
|
m_sampleCount = m_dataSize / m_sampleSize;
|
|
break;
|
|
}
|
|
|
|
// keep true decoded samples because cannot be easily determined
|
|
m_numDecodedSamples = pHeader->numDecodedSamples;
|
|
|
|
m_bNoSentence = true;
|
|
|
|
CUtlBuffer fileBuffer;
|
|
if ( pHeader->staticDataSize )
|
|
{
|
|
// get optional data
|
|
if ( !g_pFullFileSystem->ReadFile( szFullName, "GAME", fileBuffer, pHeader->staticDataSize, sizeof( xwvHeader_t ) ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
unsigned char *pData = (unsigned char *)fileBuffer.Base() + sizeof( xwvHeader_t );
|
|
if ( pHeader->GetSeekTableSize() )
|
|
{
|
|
// store off the seek table
|
|
m_nHeaderSize = pHeader->GetSeekTableSize();
|
|
m_pHeader = new char[m_nHeaderSize];
|
|
V_memcpy( m_pHeader, pData, m_nHeaderSize );
|
|
|
|
// advance past optional seek table
|
|
pData += m_nHeaderSize;
|
|
}
|
|
|
|
if ( pHeader->vdatSize )
|
|
{
|
|
m_pTempSentence = new CSentence();
|
|
Assert( m_pTempSentence );
|
|
m_bNoSentence = false;
|
|
|
|
// vdat is precompiled into minimal binary format and possibly compressed
|
|
CLZMA lzma;
|
|
|
|
if ( lzma.IsCompressed( pData ) )
|
|
{
|
|
// uncompress binary vdat and restore
|
|
CUtlBuffer targetBuffer;
|
|
int originalSize = lzma.GetActualSize( pData );
|
|
targetBuffer.EnsureCapacity( originalSize );
|
|
lzma.Uncompress( pData, (unsigned char *)targetBuffer.Base() );
|
|
targetBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, originalSize );
|
|
m_pTempSentence->CacheRestoreFromBuffer( targetBuffer );
|
|
}
|
|
else
|
|
{
|
|
m_pTempSentence->CacheRestoreFromBuffer( fileBuffer );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Bastardized construction routine. This is just to avoid complex
|
|
// constructor functions so code can be shared more easily by sub-classes
|
|
// Input : *pFormatBuffer - RIFF header
|
|
// formatSize - header size
|
|
// &walk - RIFF file
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::Setup( const char *pFormatBuffer, int formatSize, IterateRIFF &walk )
|
|
{
|
|
Init( pFormatBuffer, formatSize );
|
|
|
|
while ( walk.ChunkAvailable() )
|
|
{
|
|
ParseChunk( walk, walk.ChunkName() );
|
|
walk.ChunkNext();
|
|
}
|
|
}
|
|
|
|
|
|
bool CAudioSourceWave::GetStartupData( int &nfileSize )
|
|
{
|
|
char formatBuffer[1024];
|
|
char nameBuf[MAX_PATH];
|
|
const char *pName = m_pSfx->GetFileName(nameBuf, sizeof(nameBuf));
|
|
InFileRIFF riff( pName, *g_pSndIO );
|
|
nfileSize = riff.GetFileSize();
|
|
|
|
if ( riff.RIFFName() != RIFF_WAVE )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// set up the iterator for the whole file (root RIFF is a chunk)
|
|
IterateRIFF walk( riff, riff.RIFFSize() );
|
|
|
|
int format = 0;
|
|
int formatSize = 0;
|
|
|
|
// This chunk must be first as it contains the wave's format
|
|
// break out when we've parsed it
|
|
while ( walk.ChunkAvailable() && format == 0 )
|
|
{
|
|
switch( walk.ChunkName() )
|
|
{
|
|
case WAVE_FMT:
|
|
{
|
|
if ( walk.ChunkSize() <= sizeof( formatBuffer ) )
|
|
{
|
|
walk.ChunkRead( formatBuffer );
|
|
formatSize = walk.ChunkSize();
|
|
format = ((WAVEFORMATEX *)formatBuffer)->wFormatTag;
|
|
if( ((WAVEFORMATEX *)formatBuffer)->wBitsPerSample > 16)
|
|
{
|
|
Warning("Unsupported %d-bit wave file %s\n", (int)((WAVEFORMATEX *)formatBuffer)->wBitsPerSample, pName);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
{
|
|
ChunkError( walk.ChunkName() );
|
|
}
|
|
break;
|
|
}
|
|
walk.ChunkNext();
|
|
}
|
|
|
|
// Not really a WAVE file or no format chunk, bail
|
|
if ( !format )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Setup( formatBuffer, formatSize, walk );
|
|
|
|
if ( !m_dataStart || !m_dataSize )
|
|
{
|
|
// failed during setup
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: parses loop information from a cue chunk
|
|
// Input : &walk - RIFF iterator
|
|
// Output : int loop start position
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ParseCueChunk( IterateRIFF &walk )
|
|
{
|
|
// Cue chunk as specified by RIFF format
|
|
// see $/research/jay/sound/riffnew.htm
|
|
struct
|
|
{
|
|
unsigned int dwName;
|
|
unsigned int dwPosition;
|
|
unsigned int fccChunk;
|
|
unsigned int dwChunkStart;
|
|
unsigned int dwBlockStart;
|
|
unsigned int dwSampleOffset;
|
|
} cue_chunk;
|
|
|
|
int cueCount;
|
|
|
|
// assume that the cue chunk stored in the wave is the start of the loop
|
|
// assume only one cue chunk, UNDONE: Test this assumption here?
|
|
cueCount = walk.ChunkReadInt();
|
|
if ( cueCount > 0 )
|
|
{
|
|
walk.ChunkReadPartial( &cue_chunk, sizeof(cue_chunk) );
|
|
m_loopStart = LittleLong( cue_chunk.dwSampleOffset );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: parses loop information from a 'smpl' chunk
|
|
// Input : &walk - RIFF iterator
|
|
// Output : int loop start position
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ParseSamplerChunk( IterateRIFF &walk )
|
|
{
|
|
// Sampler chunk for MIDI instruments
|
|
// Parse loop info from this chunk too
|
|
struct SampleLoop
|
|
{
|
|
unsigned int dwIdentifier;
|
|
unsigned int dwType;
|
|
unsigned int dwStart;
|
|
unsigned int dwEnd;
|
|
unsigned int dwFraction;
|
|
unsigned int dwPlayCount;
|
|
};
|
|
|
|
struct
|
|
{
|
|
unsigned int dwManufacturer;
|
|
unsigned int dwProduct;
|
|
unsigned int dwSamplePeriod;
|
|
unsigned int dwMIDIUnityNote;
|
|
unsigned int dwMIDIPitchFraction;
|
|
unsigned int dwSMPTEFormat;
|
|
unsigned int dwSMPTEOffset;
|
|
unsigned int cSampleLoops;
|
|
unsigned int cbSamplerData;
|
|
struct SampleLoop Loops[1];
|
|
} samplerChunk;
|
|
|
|
// assume that the loop end is the sample end
|
|
// assume that only the first loop is relevant
|
|
|
|
walk.ChunkReadPartial( &samplerChunk, sizeof(samplerChunk) );
|
|
if ( LittleLong( samplerChunk.cSampleLoops ) > 0 )
|
|
{
|
|
// only support normal forward loops
|
|
if ( LittleLong( samplerChunk.Loops[0].dwType ) == 0 )
|
|
{
|
|
m_loopStart = LittleLong( samplerChunk.Loops[0].dwStart );
|
|
}
|
|
#ifdef _DEBUG
|
|
else
|
|
{
|
|
char nameBuf[MAX_PATH];
|
|
Msg("Unknown sampler chunk type %d on %s\n", LittleLong( samplerChunk.Loops[0].dwType ), m_pSfx->GetFileName(nameBuf,sizeof(nameBuf)) );
|
|
}
|
|
#endif
|
|
}
|
|
// else discard - this is some other non-loop sampler data we don't support
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: get the wave header
|
|
//-----------------------------------------------------------------------------
|
|
void *CAudioSourceWave::GetHeader( void )
|
|
{
|
|
return m_pHeader;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Gets the looping information. Some parameters are interpreted based on format
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceWave::GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples )
|
|
{
|
|
if ( pLoopBlock )
|
|
{
|
|
// for xma, the block that contains the loop point
|
|
*pLoopBlock = m_loopBlock;
|
|
}
|
|
|
|
if ( pNumLeadingSamples )
|
|
{
|
|
// for xma, the number of leading samples at the loop block to discard
|
|
*pNumLeadingSamples = m_numLeadingSamples;
|
|
}
|
|
|
|
if ( pNumTrailingSamples )
|
|
{
|
|
// for xma, the number of trailing samples at the final block to discard
|
|
*pNumTrailingSamples = m_numTrailingSamples;
|
|
}
|
|
|
|
// the loop point in samples
|
|
return m_loopStart;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: wrap the position wrt looping
|
|
// Input : samplePosition - absolute position
|
|
// Output : int - looped position
|
|
//-----------------------------------------------------------------------------
|
|
int64 CAudioSourceWave::ConvertLoopedPosition( int64 samplePosition )
|
|
{
|
|
switch ( m_format )
|
|
{
|
|
case WAVE_FORMAT_XMA:
|
|
case WAVE_FORMAT_MP3:
|
|
case WAVE_FORMAT_TEMP:
|
|
// xma and mp3 mixer interprets loops and *always* sends a corrected position
|
|
return samplePosition;
|
|
}
|
|
|
|
// if the wave is looping and we're past the end of the sample
|
|
// convert to a position within the loop
|
|
// At the end of the loop, we return a short buffer, and subsequent call
|
|
// will loop back and get the rest of the buffer
|
|
if ( m_loopStart >= 0 && samplePosition >= m_sampleCount )
|
|
{
|
|
// size of loop
|
|
int loopSize = m_sampleCount - m_loopStart;
|
|
// subtract off starting bit of the wave
|
|
samplePosition -= m_loopStart;
|
|
|
|
if ( loopSize )
|
|
{
|
|
// "real" position in memory (mod off extra loops)
|
|
samplePosition = m_loopStart + (samplePosition % loopSize);
|
|
}
|
|
// ERROR? if no loopSize
|
|
}
|
|
|
|
return samplePosition;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: remove the reference for the mixer getting deleted
|
|
// Input : *pMixer -
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ReferenceRemove( CAudioMixer *pMixer )
|
|
{
|
|
m_refCount--;
|
|
|
|
if ( m_refCount == 0 && ( ( IsPC() && IsPlayOnce() ) || ( IsGameConsole() && IsStreaming() ) ) )
|
|
{
|
|
SetPlayOnce( false ); // in case it gets used again
|
|
CacheUnload();
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Add a mixer reference
|
|
// Input : *pMixer -
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceWave::ReferenceAdd( CAudioMixer *pMixer )
|
|
{
|
|
m_refCount++;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: return true if no mixers reference this source
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceWave::CanDelete( void )
|
|
{
|
|
if ( m_refCount > 0 )
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// CAudioSourceMemWave is a bunch of wave data that is all in memory.
|
|
// To use it:
|
|
// - derive from CAudioSourceMemWave
|
|
// - call CAudioSourceWave::Init with a WAVEFORMATEX
|
|
// - set m_sampleCount.
|
|
// - implement GetDataPointer
|
|
class CAudioSourceMemWave : public CAudioSourceWave
|
|
{
|
|
public:
|
|
CAudioSourceMemWave();
|
|
CAudioSourceMemWave( CSfxTable *pSfx );
|
|
CAudioSourceMemWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info );
|
|
virtual ~CAudioSourceMemWave();
|
|
|
|
// These are all implemented by CAudioSourceMemWave.
|
|
virtual CAudioMixer* CreateMixer( int initialStreamPosition, int skipInitialSamples, bool bUpdateDelayForChoreo, SoundError &soundError, hrtf_info_t* pHRTFVec );
|
|
virtual int GetOutputData( void **pData, int64 samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] );
|
|
virtual int ZeroCrossingBefore( int sample );
|
|
virtual int ZeroCrossingAfter( int sample );
|
|
|
|
virtual int GetCacheStatus( void );
|
|
virtual void CacheLoad( void );
|
|
virtual void CacheUnload( void );
|
|
|
|
// by definition, should already be in memory
|
|
virtual void Prefetch() {}
|
|
|
|
virtual void ParseChunk( IterateRIFF &walk, int chunkName );
|
|
void ParseDataChunk( IterateRIFF &walk );
|
|
|
|
protected:
|
|
|
|
// Whoeover derives must implement this.
|
|
virtual char *GetDataPointer( void );
|
|
|
|
WaveCacheHandle_t m_hCache;
|
|
StreamHandle_t m_hStream;
|
|
|
|
private:
|
|
CAudioSourceMemWave( const CAudioSourceMemWave & ); // not implemented, not accessible
|
|
};
|
|
|
|
|
|
CAudioSourceMemWave::CAudioSourceMemWave() :
|
|
CAudioSourceWave( NULL )
|
|
{
|
|
m_hCache = 0;
|
|
m_hStream = INVALID_STREAM_HANDLE;
|
|
}
|
|
|
|
CAudioSourceMemWave::CAudioSourceMemWave( CSfxTable *pSfx ) :
|
|
CAudioSourceWave( pSfx )
|
|
{
|
|
m_hCache = 0;
|
|
m_hStream = INVALID_STREAM_HANDLE;
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
bool bValid = GetXboxAudioStartupData();
|
|
if ( !bValid )
|
|
{
|
|
// failed, substitute placeholder
|
|
pSfx->m_bUseErrorFilename = true;
|
|
bValid = GetXboxAudioStartupData();
|
|
if ( bValid )
|
|
{
|
|
char nameBuf1[MAX_PATH];
|
|
char nameBuf2[MAX_PATH];
|
|
DevWarning( "Failed to load sound \"%s\", substituting \"%s\"\n", pSfx->getname(nameBuf1,sizeof(nameBuf1)), pSfx->GetFileName(nameBuf2,sizeof(nameBuf2)) );
|
|
}
|
|
}
|
|
|
|
if ( bValid )
|
|
{
|
|
// a 360 memory wave is a critical resource kept locked in memory, load its data now
|
|
CacheLoad();
|
|
}
|
|
}
|
|
}
|
|
|
|
CAudioSourceMemWave::CAudioSourceMemWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) :
|
|
CAudioSourceWave( pSfx, info )
|
|
{
|
|
m_hCache = 0;
|
|
m_hStream = INVALID_STREAM_HANDLE;
|
|
}
|
|
|
|
CAudioSourceMemWave::~CAudioSourceMemWave()
|
|
{
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Creates a mixer and initializes it with an appropriate mixer
|
|
//-----------------------------------------------------------------------------
|
|
CAudioMixer *CAudioSourceMemWave::CreateMixer( int initialStreamPosition, int skipInitialSamples, bool bUpdateDelayForChoreo, SoundError &soundError, hrtf_info_t* pHRTFVector )
|
|
{
|
|
if (pHRTFVector && m_bits != 16)
|
|
{
|
|
char filename[256];
|
|
this->m_pSfx->GetFileName(filename, sizeof(filename));
|
|
DevMsg("Sound %s configured to use HRTF but is not a 16-bit sound\n", filename);
|
|
pHRTFVector = nullptr;
|
|
}
|
|
|
|
CAudioMixer *pMixer = CreateWaveMixer( CreateWaveDataHRTF(CreateWaveDataMemory(*this), pHRTFVector), m_format, pHRTFVector ? 2 : m_channels, m_bits, initialStreamPosition, skipInitialSamples, bUpdateDelayForChoreo );
|
|
if ( pMixer )
|
|
{
|
|
ReferenceAdd( pMixer );
|
|
soundError = SE_OK;
|
|
}
|
|
else
|
|
{
|
|
soundError = SE_CANT_CREATE_MIXER;
|
|
}
|
|
return pMixer;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : **pData - output pointer to samples
|
|
// samplePosition - position (in samples not bytes)
|
|
// sampleCount - number of samples (not bytes)
|
|
// Output : int - number of samples available
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceMemWave::GetOutputData( void **pData, int64 samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
|
|
{
|
|
// handle position looping
|
|
samplePosition = ConvertLoopedPosition( samplePosition );
|
|
|
|
// how many samples are available (linearly not counting looping)
|
|
int totalSampleCount = m_sampleCount - samplePosition;
|
|
|
|
// may be asking for a sample out of range, clip at zero
|
|
if ( totalSampleCount < 0 )
|
|
{
|
|
totalSampleCount = 0;
|
|
}
|
|
|
|
// clip max output samples to max available
|
|
if ( sampleCount > totalSampleCount )
|
|
{
|
|
sampleCount = totalSampleCount;
|
|
}
|
|
|
|
// byte offset in sample database
|
|
samplePosition *= m_sampleSize;
|
|
|
|
// if we are returning some samples, store the pointer
|
|
if ( sampleCount )
|
|
{
|
|
// Starting past end of "preloaded" data, just use regular cache
|
|
if ( samplePosition >= m_nCachedDataSize )
|
|
{
|
|
*pData = GetDataPointer();
|
|
}
|
|
else
|
|
{
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
// Start async loader if we haven't already done so
|
|
CacheLoad();
|
|
|
|
// Return less data if we are about to run out of uncached data
|
|
if ( samplePosition + ( sampleCount * m_sampleSize ) >= m_nCachedDataSize )
|
|
{
|
|
sampleCount = ( m_nCachedDataSize - samplePosition ) / m_sampleSize;
|
|
}
|
|
|
|
// Point at preloaded/cached data from .cache file for now
|
|
*pData = GetCachedDataPointer();
|
|
}
|
|
else
|
|
{
|
|
// for 360, memory wave data should have already been loaded and locked in cache
|
|
Assert( 0 );
|
|
}
|
|
}
|
|
|
|
if ( *pData )
|
|
{
|
|
*pData = (char *)*pData + samplePosition;
|
|
}
|
|
else
|
|
{
|
|
// End of data or some other problem
|
|
sampleCount = 0;
|
|
}
|
|
}
|
|
|
|
return sampleCount;
|
|
}
|
|
|
|
|
|
// Hardcoded macros to test for zero crossing
|
|
#define ZERO_X_8(b) ((b)<8 && (b)>-8)
|
|
#define ZERO_X_16(b) ((b)<2048 && (b)>-2048)
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Search backward for a zero crossing starting at sample
|
|
// Input : sample - starting point
|
|
// Output : position of zero crossing
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceMemWave::ZeroCrossingBefore( int sample )
|
|
{
|
|
char *pWaveData = GetDataPointer();
|
|
|
|
if ( m_format == WAVE_FORMAT_PCM )
|
|
{
|
|
if ( m_bits == 8 )
|
|
{
|
|
char *pData = pWaveData + sample * m_sampleSize;
|
|
bool zero = false;
|
|
|
|
if ( m_channels == 1 )
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_8(*pData) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample--;
|
|
pData--;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_8(*pData) && ZERO_X_8(pData[1]) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample--;
|
|
pData--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
short *pData = (short *)(pWaveData + sample * m_sampleSize);
|
|
bool zero = false;
|
|
|
|
if ( m_channels == 1 )
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_16(*pData) )
|
|
zero = true;
|
|
else
|
|
{
|
|
pData--;
|
|
sample--;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_16(*pData) && ZERO_X_16(pData[1]) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample--;
|
|
pData--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sample;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Search forward for a zero crossing
|
|
// Input : sample - starting point
|
|
// Output : position of found zero crossing
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceMemWave::ZeroCrossingAfter( int sample )
|
|
{
|
|
char *pWaveData = GetDataPointer();
|
|
Assert( pWaveData );
|
|
if ( !pWaveData )
|
|
return sample;
|
|
|
|
if ( m_format == WAVE_FORMAT_PCM )
|
|
{
|
|
if ( m_bits == 8 )
|
|
{
|
|
char *pData = pWaveData + sample * m_sampleSize;
|
|
bool zero = false;
|
|
|
|
if ( m_channels == 1 )
|
|
{
|
|
while ( sample < SampleCount() && !zero )
|
|
{
|
|
if ( ZERO_X_8(*pData) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample++;
|
|
pData++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while ( sample < SampleCount() && !zero )
|
|
{
|
|
if ( ZERO_X_8(*pData) && ZERO_X_8(pData[1]) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample++;
|
|
pData++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
short *pData = (short *)(pWaveData + sample * m_sampleSize);
|
|
bool zero = false;
|
|
|
|
if ( m_channels == 1 )
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_16(*pData) )
|
|
zero = true;
|
|
else
|
|
{
|
|
pData++;
|
|
sample++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
while ( sample > 0 && !zero )
|
|
{
|
|
if ( ZERO_X_16(*pData) && ZERO_X_16(pData[1]) )
|
|
zero = true;
|
|
else
|
|
{
|
|
sample++;
|
|
pData++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return sample;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: parse chunks with unique processing to in-memory waves
|
|
// Input : &walk - RIFF file
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceMemWave::ParseChunk( IterateRIFF &walk, int chunkName )
|
|
{
|
|
switch( chunkName )
|
|
{
|
|
// this is the audio data
|
|
case WAVE_DATA:
|
|
ParseDataChunk( walk );
|
|
return;
|
|
}
|
|
|
|
CAudioSourceWave::ParseChunk( walk, chunkName );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: reads the actual sample data and parses it
|
|
// Input : &walk - RIFF file
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceMemWave::ParseDataChunk( IterateRIFF &walk )
|
|
{
|
|
m_dataStart = walk.ChunkFilePosition() + 8;
|
|
m_dataSize = walk.ChunkSize();
|
|
|
|
// 360 streaming model loads data later, but still needs critical member setup
|
|
char *pData = NULL;
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
pData = GetDataPointer();
|
|
if ( !pData )
|
|
{
|
|
char nameBuf[MAX_PATH];
|
|
Error( "CAudioSourceMemWave (%s): GetDataPointer() failed.", m_pSfx ? m_pSfx->GetFileName(nameBuf, sizeof(nameBuf)) : "m_pSfx = NULL" );
|
|
}
|
|
|
|
// load them into memory (bad!!, this is a duplicate read of the data chunk)
|
|
walk.ChunkRead( pData );
|
|
}
|
|
|
|
if ( m_format == WAVE_FORMAT_PCM )
|
|
{
|
|
// number of samples loaded
|
|
m_sampleCount = m_dataSize / m_sampleSize;
|
|
m_numDecodedSamples = m_sampleCount;
|
|
}
|
|
else if ( m_format == WAVE_FORMAT_ADPCM )
|
|
{
|
|
// The ADPCM mixers treat the wave source as a flat file of bytes.
|
|
// Since each "sample" is a byte (this is a flat file), the number of samples is the file size
|
|
m_sampleCount = m_dataSize;
|
|
m_sampleSize = 1;
|
|
|
|
// file says 4, output is 16
|
|
m_bits = 16;
|
|
|
|
m_numDecodedSamples = ADPCMSampleCount( (ADPCMWAVEFORMAT *)m_pHeader, m_dataSize );
|
|
}
|
|
|
|
// some samples need to be converted
|
|
if ( pData )
|
|
{
|
|
ConvertSamples( pData, m_sampleCount );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceMemWave::GetCacheStatus( void )
|
|
{
|
|
VPROF("CAudioSourceMemWave::GetCacheStatus");
|
|
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
// NOTE: This will start the load if it isn't started
|
|
bool bCacheValid, bIsMissing;
|
|
bool bCompleted = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid, &bIsMissing );
|
|
if ( !bCacheValid )
|
|
{
|
|
char nameBuf[MAX_PATH];
|
|
wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(nameBuf, sizeof(nameBuf)), m_dataSize, m_dataStart );
|
|
}
|
|
if ( bCompleted )
|
|
return AUDIO_IS_LOADED;
|
|
if ( bIsMissing )
|
|
return AUDIO_ERROR_LOADING;
|
|
if ( wavedatacache->IsDataLoadInProgress( m_hCache ) )
|
|
return AUDIO_LOADING;
|
|
}
|
|
else
|
|
{
|
|
return wavedatacache->IsStreamedDataReady( m_hStream ) ? AUDIO_IS_LOADED : AUDIO_NOT_LOADED;
|
|
}
|
|
|
|
return AUDIO_NOT_LOADED;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceMemWave::CacheLoad( void )
|
|
{
|
|
char nameBuf[MAX_PATH];
|
|
if ( IsPC() )
|
|
{
|
|
// Commence lazy load?
|
|
if ( m_hCache != 0 )
|
|
{
|
|
bool bCacheValid;
|
|
wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid );
|
|
if ( !bCacheValid )
|
|
{
|
|
wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(nameBuf, sizeof(nameBuf)), m_dataSize, m_dataStart );
|
|
}
|
|
return;
|
|
}
|
|
|
|
m_hCache = wavedatacache->AsyncLoadCache( m_pSfx->GetFileName(nameBuf,sizeof(nameBuf)), m_dataSize, m_dataStart );
|
|
}
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
if ( m_hStream == INVALID_STREAM_HANDLE )
|
|
{
|
|
// memory wave is resident
|
|
const char *pFilename = m_pSfx->GetFileName(nameBuf, sizeof(nameBuf));
|
|
streamFlags_t streamFlags = STREAMED_FROMDVD;
|
|
char szFilename[MAX_PATH];
|
|
|
|
switch ( m_format )
|
|
{
|
|
case WAVE_FORMAT_XMA:
|
|
case WAVE_FORMAT_PCM:
|
|
case WAVE_FORMAT_MP3:
|
|
case WAVE_FORMAT_TEMP:
|
|
strcpy( szFilename, pFilename );
|
|
V_SetExtension( szFilename, PLATFORM_EXT ".wav", sizeof( szFilename ) );
|
|
pFilename = szFilename;
|
|
|
|
// memory resident xma waves use the queued loader
|
|
// restricting to XMA due to not correctly running a post ConvertSamples, which is not an issue for XMA
|
|
if ( g_pQueuedLoader->IsMapLoading() )
|
|
{
|
|
// hint the wave data cache
|
|
// these are map based static sounds pooled accordingly
|
|
streamFlags |= STREAMED_QUEUEDLOAD;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Normal mode for the other files...
|
|
break;
|
|
}
|
|
|
|
// open stream to load as a single monolithic buffer
|
|
SoundError error;
|
|
m_hStream = wavedatacache->OpenStreamedLoad( pFilename, m_dataSize, m_dataStart, 0, -1, m_dataSize, 1, streamFlags, error );
|
|
if ( m_hStream != INVALID_STREAM_HANDLE && !( streamFlags & STREAMED_QUEUEDLOAD ) )
|
|
{
|
|
// causes a synchronous block to finish the load
|
|
// convert data once right now
|
|
char *pWaveData = (char *)wavedatacache->GetStreamedDataPointer( m_hStream, true );
|
|
if ( pWaveData )
|
|
{
|
|
ConvertSamples( pWaveData, m_dataSize/m_sampleSize );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceMemWave::CacheUnload( void )
|
|
{
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
if ( m_hCache != 0 )
|
|
{
|
|
wavedatacache->Unload( m_hCache );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( m_hStream != INVALID_STREAM_HANDLE )
|
|
{
|
|
wavedatacache->CloseStreamedLoad( m_hStream );
|
|
m_hStream = INVALID_STREAM_HANDLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : char
|
|
//-----------------------------------------------------------------------------
|
|
char *CAudioSourceMemWave::GetDataPointer( void )
|
|
{
|
|
char *pWaveData = NULL;
|
|
|
|
char nameBuf[MAX_PATH];
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
bool bSamplesConverted = false;
|
|
|
|
if ( m_hCache == 0 )
|
|
{
|
|
// not in cache, start loading
|
|
CacheLoad();
|
|
}
|
|
|
|
// mount the requested data, blocks if necessary
|
|
// TERROR: limit data read while rebuilding cache
|
|
int dataSize = m_dataSize;
|
|
if ( m_bIsRebuildingCache )
|
|
{
|
|
const char *filename = m_pSfx->GetFileName(nameBuf, sizeof(nameBuf));
|
|
if ( V_stristr( filename, "music" ) != NULL )
|
|
{
|
|
dataSize = MIN( dataSize, 32768 );
|
|
}
|
|
}
|
|
wavedatacache->GetDataPointer(
|
|
m_hCache,
|
|
m_pSfx->GetFileName(nameBuf, sizeof(nameBuf)),
|
|
dataSize,
|
|
m_dataStart,
|
|
(void **)&pWaveData,
|
|
0,
|
|
&bSamplesConverted );
|
|
|
|
// If we have reloaded data from disk (async) and we haven't converted the samples yet, do it now
|
|
// FIXME: Is this correct for stereo wavs?
|
|
if ( pWaveData && !bSamplesConverted )
|
|
{
|
|
ConvertSamples( pWaveData, m_dataSize/m_sampleSize );
|
|
wavedatacache->SetPostProcessed( m_hCache, true );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( m_hStream != INVALID_STREAM_HANDLE )
|
|
{
|
|
// expected to be valid, unless failure during setup
|
|
pWaveData = (char *)wavedatacache->GetStreamedDataPointer( m_hStream, true );
|
|
}
|
|
}
|
|
|
|
return pWaveData;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Wave source for streaming wave files
|
|
// UNDONE: Handle looping
|
|
//-----------------------------------------------------------------------------
|
|
class CAudioSourceStreamWave : public CAudioSourceWave, public IWaveStreamSource
|
|
{
|
|
public:
|
|
CAudioSourceStreamWave( CSfxTable *pSfx );
|
|
CAudioSourceStreamWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info );
|
|
virtual ~CAudioSourceStreamWave();
|
|
|
|
CAudioMixer *CreateMixer( int initialStreamPosition, int skipInitialSamples, bool bUpdateDelayForChoreo, SoundError &soundError, hrtf_info_t *pHRTFVec );
|
|
int GetOutputData( void **pData, int64 samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] );
|
|
void ParseChunk( IterateRIFF &walk, int chunkName );
|
|
bool IsStreaming( void ) { return true; }
|
|
|
|
virtual int GetCacheStatus( void );
|
|
|
|
// IWaveStreamSource
|
|
virtual int64 UpdateLoopingSamplePosition( int64 samplePosition )
|
|
{
|
|
return ConvertLoopedPosition( samplePosition );
|
|
}
|
|
virtual void UpdateSamples( char *pData, int sampleCount )
|
|
{
|
|
ConvertSamples( pData, sampleCount );
|
|
}
|
|
virtual int GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples )
|
|
{
|
|
return CAudioSourceWave::GetLoopingInfo( pLoopBlock, pNumLeadingSamples, pNumTrailingSamples );
|
|
}
|
|
|
|
virtual void Prefetch();
|
|
|
|
virtual int SampleToStreamPosition( int samplePosition );
|
|
virtual int StreamToSamplePosition( int streamPosition );
|
|
|
|
private:
|
|
CAudioSourceStreamWave( const CAudioSourceStreamWave & ); // not implemented, not accessible
|
|
|
|
#if !defined( _GAMECONSOLE )
|
|
// We need this for -tools mode to get access to the raw samples
|
|
FileHandle_t m_hWaveFileAccess;
|
|
#endif
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Save a copy of the file name for instances to open later
|
|
// Input : *pFileName - filename
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceStreamWave::CAudioSourceStreamWave( CSfxTable *pSfx ) : CAudioSourceWave( pSfx )
|
|
{
|
|
m_pSfx = pSfx;
|
|
m_dataStart = -1;
|
|
m_dataSize = 0;
|
|
m_sampleCount = 0;
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
bool bValid = GetXboxAudioStartupData();
|
|
if ( !bValid )
|
|
{
|
|
// failed, substitute placeholder
|
|
pSfx->m_bUseErrorFilename = true;
|
|
bValid = GetXboxAudioStartupData();
|
|
if ( bValid )
|
|
{
|
|
char nameBuf1[MAX_PATH];
|
|
char nameBuf2[MAX_PATH];
|
|
DevWarning( "Failed to load sound \"%s\", substituting \"%s\"\n", pSfx->getname(nameBuf1,sizeof(nameBuf1)), pSfx->GetFileName(nameBuf2,sizeof(nameBuf2)) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CAudioSourceStreamWave::CAudioSourceStreamWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) :
|
|
CAudioSourceWave( pSfx, info )
|
|
#if !defined( _GAMECONSOLE )
|
|
,m_hWaveFileAccess( FILESYSTEM_INVALID_HANDLE )
|
|
#endif
|
|
{
|
|
m_pSfx = pSfx;
|
|
m_dataStart = info->DataStart();
|
|
m_dataSize = info->DataSize();
|
|
|
|
m_sampleCount = info->SampleCount();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: free the filename buffer
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceStreamWave::~CAudioSourceStreamWave( void )
|
|
{
|
|
#if !defined( _GAMECONSOLE )
|
|
if ( m_hWaveFileAccess != FILESYSTEM_INVALID_HANDLE )
|
|
{
|
|
g_pFullFileSystem->Close( m_hWaveFileAccess );
|
|
m_hWaveFileAccess = FILESYSTEM_INVALID_HANDLE;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Create an instance (mixer & wavedata) of this sound
|
|
// Output : CAudioMixer * - pointer to the mixer
|
|
//-----------------------------------------------------------------------------
|
|
CAudioMixer *CAudioSourceStreamWave::CreateMixer( int initialStreamPosition, int skipInitialSamples, bool bUpdateDelayForChoreo, SoundError &soundError, hrtf_info_t* pHRTFVec )
|
|
{
|
|
char fileName[MAX_PATH];
|
|
const char *pFileName = m_pSfx->GetFileName(fileName, sizeof(fileName));
|
|
if ( IsGameConsole() )
|
|
{
|
|
switch ( m_format )
|
|
{
|
|
case WAVE_FORMAT_XMA:
|
|
#if IsX360()
|
|
// for safety, validate the initial stream position
|
|
// not trusting save/load
|
|
if ( ( initialStreamPosition % XBOX_DVD_SECTORSIZE ) ||
|
|
( initialStreamPosition % XMA_BLOCK_SIZE ) ||
|
|
( initialStreamPosition >= m_dataSize ) )
|
|
{
|
|
initialStreamPosition = 0;
|
|
}
|
|
#endif
|
|
// Pass through...
|
|
case WAVE_FORMAT_PCM:
|
|
case WAVE_FORMAT_MP3:
|
|
case WAVE_FORMAT_TEMP:
|
|
V_SetExtension( fileName, PLATFORM_EXT ".wav", sizeof( fileName ) );
|
|
break;
|
|
|
|
default:
|
|
// Do nothing otherwise
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (pHRTFVec && m_bits != 16)
|
|
{
|
|
char filename[256];
|
|
this->m_pSfx->GetFileName(filename, sizeof(filename));
|
|
DevMsg("Sound %s configured to use HRTF but is not a 16-bit sound\n", filename);
|
|
pHRTFVec = nullptr;
|
|
}
|
|
|
|
// BUGBUG: Source constructs the IWaveData, mixer frees it, fix this?
|
|
IWaveData *pWaveData = CreateWaveDataHRTF(CreateWaveDataStream( *this, static_cast<IWaveStreamSource *>(this), pFileName, m_dataStart, m_dataSize, m_pSfx, initialStreamPosition, skipInitialSamples, soundError ), pHRTFVec);
|
|
if ( pWaveData )
|
|
{
|
|
CAudioMixer *pMixer = CreateWaveMixer( pWaveData, m_format, pHRTFVec ? 2 : m_channels, m_bits, initialStreamPosition, skipInitialSamples, bUpdateDelayForChoreo );
|
|
if ( pMixer )
|
|
{
|
|
ReferenceAdd( pMixer );
|
|
return pMixer;
|
|
}
|
|
|
|
// no mixer, delete the stream buffer/instance
|
|
delete pWaveData;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void CAudioSourceStreamWave::Prefetch()
|
|
{
|
|
char nameBuf[MAX_PATH];
|
|
PrefetchDataStream( m_pSfx->GetFileName(nameBuf, sizeof(nameBuf)), m_dataStart, m_dataSize );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceStreamWave::SampleToStreamPosition( int samplePosition )
|
|
{
|
|
switch ( m_format )
|
|
{
|
|
case WAVE_FORMAT_XMA:
|
|
if ( m_nHeaderSize != 0 )
|
|
{
|
|
// Run through the seek table to find the block closest to the desired sample.
|
|
// Each seek table entry is the index (counting from the beginning of the file)
|
|
// of the first sample in the corresponding block, but there's no entry for the
|
|
// first block (since the index would always be zero).
|
|
int *pSeekTable = (int*)m_pHeader;
|
|
int packet = 0;
|
|
for ( int i = 0; i < m_nHeaderSize/(int)sizeof( int ); ++i )
|
|
{
|
|
if ( samplePosition < pSeekTable[i] )
|
|
{
|
|
packet = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
int streamPosition = ( packet == 0 ) ? 0 : ( packet - 1 ) * 2048;
|
|
return streamPosition;
|
|
}
|
|
break;
|
|
|
|
case WAVE_FORMAT_PCM:
|
|
return samplePosition * m_sampleSize;
|
|
}
|
|
|
|
// Function is not supported
|
|
if ( !IsCert() )
|
|
{
|
|
char fileName[MAX_PATH];
|
|
const char *pFileName = GetFileName( fileName, sizeof( fileName ) );
|
|
Warning( "SampleToStreamPosition( %d ) is not supported for sound '%s'.\n", samplePosition, pFileName );
|
|
}
|
|
// not in the expected format or lacking the seek table
|
|
return -1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceStreamWave::StreamToSamplePosition( int streamPosition )
|
|
{
|
|
switch ( m_format )
|
|
{
|
|
case WAVE_FORMAT_XMA:
|
|
if ( m_nHeaderSize != 0 )
|
|
{
|
|
int packet = streamPosition/2048;
|
|
if ( packet <= 0 )
|
|
{
|
|
return 0;
|
|
}
|
|
if ( packet > m_nHeaderSize/(int)sizeof( int ) )
|
|
{
|
|
return m_numDecodedSamples;
|
|
}
|
|
|
|
return ((int*)m_pHeader)[packet - 1];
|
|
}
|
|
break;
|
|
|
|
case WAVE_FORMAT_PCM:
|
|
return streamPosition / m_sampleSize;
|
|
}
|
|
|
|
// Function is not supported
|
|
if ( !IsCert() )
|
|
{
|
|
char fileName[MAX_PATH];
|
|
const char *pFileName = GetFileName( fileName, sizeof( fileName ) );
|
|
Warning( "StreamToSamplePosition( %d ) is not supported for sound '%s'.\n", streamPosition, pFileName );
|
|
}
|
|
// not in the expected format or lacking the seek table
|
|
return -1;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Parse a stream wave file chunk
|
|
// unlike the in-memory file, don't load the data, just get a reference to it.
|
|
// Input : &walk - RIFF file
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceStreamWave::ParseChunk( IterateRIFF &walk, int chunkName )
|
|
{
|
|
// NOTE: It would be nice to break out of parsing once we have the data start and
|
|
// save seeking over the whole file. But to do so, the other needed chunks must occur
|
|
// before the DATA chunk. But, that is not standard and breaks most other wav parsers.
|
|
|
|
switch( chunkName )
|
|
{
|
|
case WAVE_DATA:
|
|
// data starts at chunk + 8 (chunk name, chunk size = 2*4=8 bytes)
|
|
// don't load the data, just know where it is so each instance
|
|
// can load it later
|
|
m_dataStart = walk.ChunkFilePosition() + 8;
|
|
m_dataSize = walk.ChunkSize();
|
|
m_sampleCount = m_dataSize / m_sampleSize;
|
|
return;
|
|
}
|
|
CAudioSourceWave::ParseChunk( walk, chunkName );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: This is not implemented here. This source has no data. It is the
|
|
// WaveData's responsibility to load/serve the data
|
|
//-----------------------------------------------------------------------------
|
|
int CAudioSourceStreamWave::GetOutputData( void **pData, int64 samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
|
|
{
|
|
#if !defined( _GAMECONSOLE )
|
|
// Only -tools mode uses this to build a "preview" of the wave form for PCM data only
|
|
if ( GetType() == WAVE_FORMAT_PCM )
|
|
{
|
|
if ( m_hWaveFileAccess == FILESYSTEM_INVALID_HANDLE )
|
|
{
|
|
char buf[ MAX_PATH ];
|
|
const char *pFilename = GetFileName( buf, sizeof( buf ) );
|
|
m_hWaveFileAccess = g_pFullFileSystem->Open( CFmtStr( "sound\\%s", pFilename ), "rb", "GAME" );
|
|
}
|
|
|
|
if ( m_hWaveFileAccess != FILESYSTEM_INVALID_HANDLE )
|
|
{
|
|
g_pFullFileSystem->Seek( m_hWaveFileAccess, m_dataStart + samplePosition * SampleSize(), FILESYSTEM_SEEK_HEAD );
|
|
if ( copyBuf != NULL )
|
|
{
|
|
g_pFullFileSystem->Read( copyBuf, sampleCount * SampleSize(), m_hWaveFileAccess );
|
|
}
|
|
*pData = copyBuf;
|
|
return sampleCount;
|
|
}
|
|
}
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
int CAudioSourceStreamWave::GetCacheStatus( void )
|
|
{
|
|
if ( !m_dataSize || !m_dataStart )
|
|
{
|
|
// didn't get precached properly
|
|
return AUDIO_NOT_LOADED;
|
|
}
|
|
|
|
return AUDIO_IS_LOADED;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Create a wave audio source (streaming or in memory)
|
|
// Input : *pName - file name (NOTE: CAUDIOSOURCE KEEPS A POINTER TO pSfx)
|
|
// streaming - if true, don't load, stream each instance
|
|
// Output : CAudioSource * - a new source
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSource *CreateWave( CSfxTable *pSfx, bool bStreaming )
|
|
{
|
|
Assert( pSfx );
|
|
|
|
#if defined( _DEBUG )
|
|
// For some reason you can't usually do pSfx->getname() in the dev studio debugger, so for convenience we'll grab the name
|
|
// here in debug builds at least...
|
|
char nameBuf[MAX_PATH];
|
|
char const *pName = pSfx->getname( nameBuf, sizeof(nameBuf) );
|
|
NOTE_UNUSED( pName );
|
|
#endif
|
|
|
|
CAudioSourceWave *pWave = NULL;
|
|
|
|
if ( IsPC() || !IsGameConsole() )
|
|
{
|
|
// Caching should always work, so if we failed to cache, it's a problem reading the file data, etc.
|
|
bool bIsMapSound = pSfx->IsPrecachedSound();
|
|
CAudioSourceCachedInfo *pInfo = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_WAV, bIsMapSound, pSfx );
|
|
|
|
if ( pInfo && pInfo->Type() != CAudioSource::AUDIO_SOURCE_UNK )
|
|
{
|
|
// create the source from this file
|
|
if ( bStreaming )
|
|
{
|
|
pWave = new CAudioSourceStreamWave( pSfx, pInfo );
|
|
}
|
|
else
|
|
{
|
|
pWave = new CAudioSourceMemWave( pSfx, pInfo );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 360 does not use audio cache system
|
|
// create the desired type
|
|
if ( bStreaming )
|
|
{
|
|
pWave = new CAudioSourceStreamWave( pSfx );
|
|
}
|
|
else
|
|
{
|
|
pWave = new CAudioSourceMemWave( pSfx );
|
|
}
|
|
}
|
|
|
|
if ( pWave && !pWave->Format() )
|
|
{
|
|
// lack of format indicates failure
|
|
delete pWave;
|
|
pWave = NULL;
|
|
}
|
|
|
|
return pWave;
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Wrapper for CreateWave()
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSource *Audio_CreateStreamedWave( CSfxTable *pSfx )
|
|
{
|
|
#if defined( MP3_SUPPORT )
|
|
char nameBuf[MAX_PATH];
|
|
if ( Audio_IsMP3( pSfx->GetFileName( nameBuf, sizeof(nameBuf) ) ) )
|
|
{
|
|
return Audio_CreateStreamedMP3( pSfx );
|
|
}
|
|
#endif
|
|
|
|
return CreateWave( pSfx, true );
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Wrapper for CreateWave()
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSource *Audio_CreateMemoryWave( CSfxTable *pSfx )
|
|
{
|
|
#if defined( MP3_SUPPORT )
|
|
char nameBuf[MAX_PATH];
|
|
if ( Audio_IsMP3( pSfx->GetFileName( nameBuf, sizeof(nameBuf) ) ) )
|
|
{
|
|
return Audio_CreateMemoryMP3( pSfx );
|
|
}
|
|
|
|
// In case the WAV file is actually an MP3 file.
|
|
bool bIsMapSound = pSfx->IsPrecachedSound();
|
|
// We pass AUDIO_SOURCE_WAV to not break the sound cache hack :(
|
|
CAudioSourceCachedInfo *pInfo = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_WAV, bIsMapSound, pSfx );
|
|
if ( pInfo->Type() == CAudioSource::AUDIO_SOURCE_MP3 )
|
|
{
|
|
return Audio_CreateMemoryMP3( pSfx );
|
|
}
|
|
#endif
|
|
|
|
return CreateWave( pSfx, false );
|
|
}
|
|
|
|
float GetMP3Duration_Helper( char const *filename );
|
|
static float Audio_GetMP3Duration( char const *pName )
|
|
{
|
|
// Deduce from file
|
|
return GetMP3Duration_Helper( pName );
|
|
}
|
|
|
|
|
|
void MaybeReportMissingWav( char const *wav )
|
|
{
|
|
static CUtlSymbolTable wavErrors;
|
|
|
|
CUtlSymbol sym;
|
|
sym = wavErrors.Find( wav );
|
|
if ( UTL_INVAL_SYMBOL == sym )
|
|
{
|
|
// See if file exists
|
|
if ( g_pFullFileSystem->FileExists( wav ) )
|
|
{
|
|
DevWarning( "Bad Audio file '%s'\n", wav );
|
|
}
|
|
else
|
|
{
|
|
DevWarning( "Missing wav file '%s'\n", wav );
|
|
}
|
|
wavErrors.AddString( wav );
|
|
}
|
|
}
|
|
|
|
static float Audio_GetADPCMWaveDuration( char const *pName )
|
|
{
|
|
char formatBuffer[1024];
|
|
WAVEFORMATEX *pfmt = (WAVEFORMATEX *)formatBuffer;
|
|
|
|
InFileRIFF riff( pName, *g_pSndIO );
|
|
|
|
if ( riff.RIFFName() != RIFF_WAVE )
|
|
{
|
|
MaybeReportMissingWav( pName );
|
|
return 0.0f;
|
|
}
|
|
|
|
// set up the iterator for the whole file (root RIFF is a chunk)
|
|
IterateRIFF walk( riff, riff.RIFFSize() );
|
|
|
|
int format = 0;
|
|
int formatSize = 0;
|
|
int sampleCount = 0;
|
|
|
|
// This chunk must be first as it contains the wave's format
|
|
// break out when we've parsed it
|
|
while ( walk.ChunkAvailable() && ( format == 0 || sampleCount == 0 ) )
|
|
{
|
|
switch( walk.ChunkName() )
|
|
{
|
|
case WAVE_FMT:
|
|
if ( walk.ChunkSize() <= sizeof( formatBuffer ) )
|
|
{
|
|
walk.ChunkRead( formatBuffer );
|
|
formatSize = walk.ChunkSize();
|
|
format = LittleWord( pfmt->wFormatTag );
|
|
}
|
|
break;
|
|
case WAVE_DATA:
|
|
if ( format != 0 )
|
|
{
|
|
int dataSize = walk.ChunkSize();
|
|
if ( format == WAVE_FORMAT_ADPCM )
|
|
{
|
|
// Dummy size for now
|
|
sampleCount = dataSize;
|
|
}
|
|
else
|
|
{
|
|
sampleCount = dataSize / ( LittleWord( pfmt->wBitsPerSample ) >> 3 );
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
ChunkError( walk.ChunkName() );
|
|
break;
|
|
}
|
|
walk.ChunkNext();
|
|
}
|
|
|
|
float sampleRate = LittleDWord( pfmt->nSamplesPerSec );
|
|
|
|
// Determine actual duration
|
|
sampleCount = ADPCMSampleCount( (ADPCMWAVEFORMAT *)formatBuffer, sampleCount );
|
|
|
|
return (float)sampleCount / sampleRate;
|
|
}
|
|
|
|
|
|
static float Audio_GetWaveDuration( char const *pName )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// should have precached
|
|
return 0;
|
|
}
|
|
|
|
CAudioSourceCachedInfo *pInfo = audiosourcecache->GetInfoByName( CFmtStr( "sound/%s", PSkipSoundChars( pName ) ) );
|
|
|
|
if ( !pInfo )
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
int nFormat = pInfo->Format();
|
|
int nSampleCount = pInfo->SampleCount();
|
|
int nSampleRate = pInfo->SampleRate();
|
|
|
|
// Not really a WAVE file or no format chunk, bail
|
|
if ( !nFormat || !nSampleCount )
|
|
return 0.0f;
|
|
|
|
if ( nFormat == WAVE_FORMAT_ADPCM )
|
|
{
|
|
return Audio_GetADPCMWaveDuration( pName );
|
|
}
|
|
else
|
|
{
|
|
return (float)nSampleCount / nSampleRate;
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose: Fast method for determining duration of .wav/.mp3, exposed to server as well
|
|
// Input : *pName -
|
|
// Output : float
|
|
//-----------------------------------------------------------------------------
|
|
float AudioSource_GetSoundDuration( char const *pName )
|
|
{
|
|
#if defined( MP3_SUPPORT )
|
|
if ( Audio_IsMP3( pName ) )
|
|
{
|
|
return Audio_GetMP3Duration( pName );
|
|
}
|
|
#endif
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
CSfxTable *pSound = S_PrecacheSound( pName );
|
|
if ( pSound )
|
|
{
|
|
return AudioSource_GetSoundDuration( pSound );
|
|
}
|
|
}
|
|
|
|
return Audio_GetWaveDuration( pName );
|
|
}
|
|
|
|
float AudioSource_GetSoundDuration( CSfxTable *pSfx )
|
|
{
|
|
if ( pSfx && pSfx->pSource )
|
|
{
|
|
return (float)pSfx->pSource->SampleCount() / (float)pSfx->pSource->SampleRate();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
CAudioSourceCachedInfo::CAudioSourceCachedInfo() :
|
|
infolong( 0 ),
|
|
flagsbyte( 0 ),
|
|
m_dataStart( 0 ),
|
|
m_dataSize( 0 ),
|
|
m_loopStart( 0 ),
|
|
m_sampleCount( 0 ),
|
|
m_usCachedDataSize( 0 ),
|
|
m_pCachedData( 0 ),
|
|
m_usHeaderSize( 0 ),
|
|
m_pHeader( 0 ),
|
|
m_pSentence( 0 )
|
|
{
|
|
}
|
|
|
|
CAudioSourceCachedInfo& CAudioSourceCachedInfo::operator =( const CAudioSourceCachedInfo& src )
|
|
{
|
|
if ( this == &src )
|
|
return *this;
|
|
|
|
infolong = src.infolong;
|
|
flagsbyte = src.flagsbyte;
|
|
SetDataStart( src.DataStart() );
|
|
SetDataSize( src.DataSize() );
|
|
SetLoopStart( src.LoopStart() );
|
|
SetSampleCount( src.SampleCount() );
|
|
|
|
CSentence *scopy = NULL;
|
|
if ( src.Sentence() )
|
|
{
|
|
scopy = new CSentence();
|
|
*scopy = *src.Sentence();
|
|
}
|
|
SetSentence( scopy );
|
|
|
|
byte *data = NULL;
|
|
|
|
Assert( src.CachedDataSize() == 0 || src.CachedData() );
|
|
|
|
m_usCachedDataSize = 0;
|
|
|
|
if ( src.CachedData() && src.CachedDataSize() > 0 )
|
|
{
|
|
SetCachedDataSize( src.CachedDataSize() );
|
|
data = new byte[ src.CachedDataSize() ];
|
|
Assert( data );
|
|
Q_memcpy( data, src.CachedData(), src.CachedDataSize() );
|
|
}
|
|
|
|
SetCachedData( data );
|
|
|
|
data = NULL;
|
|
|
|
Assert( src.HeaderSize() == 0 || src.HeaderData() );
|
|
|
|
m_usHeaderSize = 0;
|
|
|
|
if ( src.HeaderData() && src.HeaderSize() > 0 )
|
|
{
|
|
SetHeaderSize( src.HeaderSize() );
|
|
data = new byte[ src.HeaderSize() ];
|
|
Assert( data );
|
|
Q_memcpy( data, src.HeaderData(), src.HeaderSize() );
|
|
}
|
|
|
|
SetHeaderData( data );
|
|
|
|
return *this;
|
|
}
|
|
|
|
CAudioSourceCachedInfo::CAudioSourceCachedInfo( const CAudioSourceCachedInfo& src )
|
|
{
|
|
if ( this == &src )
|
|
{
|
|
Assert( 0 );
|
|
return;
|
|
}
|
|
|
|
infolong = src.infolong;
|
|
flagsbyte = src.flagsbyte;
|
|
SetDataStart( src.DataStart() );
|
|
SetDataSize( src.DataSize() );
|
|
SetLoopStart( src.LoopStart() );
|
|
SetSampleCount( src.SampleCount() );
|
|
|
|
CSentence *scopy = NULL;
|
|
if ( src.Sentence() )
|
|
{
|
|
scopy = new CSentence();
|
|
*scopy = *src.Sentence();
|
|
}
|
|
SetSentence( scopy );
|
|
|
|
byte *data = NULL;
|
|
|
|
Assert( src.CachedDataSize() == 0 || src.CachedData() );
|
|
|
|
m_usCachedDataSize = 0;
|
|
|
|
if ( src.CachedData() && src.CachedDataSize() > 0 )
|
|
{
|
|
SetCachedDataSize( src.CachedDataSize() );
|
|
data = new byte[ src.CachedDataSize() ];
|
|
Assert( data );
|
|
Q_memcpy( data, src.CachedData(), src.CachedDataSize() );
|
|
}
|
|
|
|
SetCachedData( data );
|
|
|
|
data = NULL;
|
|
|
|
Assert( src.HeaderSize() == 0 || src.HeaderData() );
|
|
|
|
m_usHeaderSize = 0;
|
|
|
|
if ( src.HeaderData() && src.HeaderSize() > 0 )
|
|
{
|
|
SetHeaderSize( src.HeaderSize() );
|
|
data = new byte[ src.HeaderSize() ];
|
|
Assert( data );
|
|
Q_memcpy( data, src.HeaderData(), src.HeaderSize() );
|
|
}
|
|
|
|
SetHeaderData( data );
|
|
}
|
|
|
|
CAudioSourceCachedInfo::~CAudioSourceCachedInfo()
|
|
{
|
|
Clear();
|
|
}
|
|
|
|
void CAudioSourceCachedInfo::Clear()
|
|
{
|
|
infolong = 0;
|
|
flagsbyte = 0;
|
|
m_dataStart = 0;
|
|
m_dataSize = 0;
|
|
m_loopStart = 0;
|
|
m_sampleCount = 0;
|
|
|
|
delete m_pSentence;
|
|
m_pSentence = NULL;
|
|
|
|
delete[] m_pCachedData;
|
|
m_pCachedData = NULL;
|
|
m_usCachedDataSize = 0;
|
|
|
|
delete[] m_pHeader;
|
|
m_pHeader = NULL;
|
|
m_usHeaderSize = 0;
|
|
}
|
|
|
|
void CAudioSourceCachedInfo::RemoveData()
|
|
{
|
|
delete[] m_pCachedData;
|
|
m_pCachedData = NULL;
|
|
m_usCachedDataSize = 0;
|
|
flags.m_bCachedData = false;
|
|
}
|
|
|
|
void CAudioSourceCachedInfo::Save( CUtlBuffer& buf )
|
|
{
|
|
buf.PutInt( infolong );
|
|
buf.PutChar( flagsbyte );
|
|
buf.PutInt( m_dataStart );
|
|
buf.PutInt( m_dataSize );
|
|
buf.PutInt( m_loopStart );
|
|
buf.PutInt( m_sampleCount );
|
|
|
|
if ( flags.m_bSentence )
|
|
{
|
|
m_pSentence->CacheSaveToBuffer( buf, CACHED_SENTENCE_VERSION );
|
|
}
|
|
|
|
Assert( m_usCachedDataSize < 65535 );
|
|
|
|
if ( flags.m_bCachedData && m_pCachedData )
|
|
{
|
|
buf.PutInt( m_usCachedDataSize );
|
|
buf.Put( m_pCachedData, m_usCachedDataSize );
|
|
}
|
|
|
|
Assert( m_usHeaderSize <= 32767 );
|
|
|
|
if ( flags.m_bHeader )
|
|
{
|
|
buf.PutShort( m_usHeaderSize );
|
|
buf.Put( m_pHeader, m_usHeaderSize );
|
|
}
|
|
}
|
|
|
|
void CAudioSourceCachedInfo::Restore( CUtlBuffer& buf )
|
|
{
|
|
// Wipe any old data!!!
|
|
Clear();
|
|
|
|
infolong = buf.GetInt();
|
|
flagsbyte = buf.GetChar();
|
|
m_dataStart = buf.GetInt();
|
|
m_dataSize = buf.GetInt();
|
|
m_loopStart = buf.GetInt();
|
|
m_sampleCount = buf.GetInt();
|
|
if ( flags.m_bSentence )
|
|
{
|
|
m_pSentence = new CSentence();
|
|
Assert( m_pSentence );
|
|
m_pSentence->CacheRestoreFromBuffer( buf );
|
|
}
|
|
|
|
if ( flags.m_bCachedData )
|
|
{
|
|
m_usCachedDataSize = buf.GetInt();
|
|
Assert( m_usCachedDataSize > 0 && m_usCachedDataSize < 65535 );
|
|
if ( m_usCachedDataSize > 0 )
|
|
{
|
|
byte *data = new byte[ m_usCachedDataSize ];
|
|
buf.Get( data, m_usCachedDataSize );
|
|
SetCachedData( data );
|
|
}
|
|
}
|
|
|
|
if ( flags.m_bHeader )
|
|
{
|
|
m_usHeaderSize = buf.GetShort();
|
|
Assert( m_usHeaderSize > 0 && m_usHeaderSize <= 32767 );
|
|
if ( m_usHeaderSize > 0 )
|
|
{
|
|
byte *data = new byte[ m_usHeaderSize ];
|
|
buf.Get( data, m_usHeaderSize );
|
|
SetHeaderData( data );
|
|
}
|
|
}
|
|
}
|
|
|
|
int CAudioSourceCachedInfo::s_CurrentType = CAudioSource::AUDIO_SOURCE_MAXTYPE;
|
|
CSfxTable *CAudioSourceCachedInfo::s_pSfx = NULL;
|
|
bool CAudioSourceCachedInfo::s_bIsPrecacheSound = false;
|
|
static bool g_bSoundRebuilt = false;
|
|
void CAudioSourceCachedInfo::Rebuild( char const *filename )
|
|
{
|
|
g_bSoundRebuilt = true;
|
|
// Wipe any old data
|
|
Clear();
|
|
|
|
Assert( s_pSfx );
|
|
Assert( s_CurrentType != CAudioSource::AUDIO_SOURCE_MAXTYPE );
|
|
|
|
#if 0
|
|
// Never cachify something which is not in the client precache list
|
|
if ( s_bIsPrecacheSound != s_pSfx->IsPrecachedSound() )
|
|
{
|
|
Msg( "Logic bug, precaching entry for '%s' which is not in precache list\n",
|
|
filename );
|
|
}
|
|
#endif
|
|
|
|
SetType( s_CurrentType );
|
|
|
|
CAudioSource *as = NULL;
|
|
|
|
// Note though these instantiate a specific AudioSource subclass, it doesn't matter, we just need one for .wav and one for .mp3
|
|
switch ( s_CurrentType )
|
|
{
|
|
default:
|
|
case CAudioSource::AUDIO_SOURCE_VOICE:
|
|
break;
|
|
case CAudioSource::AUDIO_SOURCE_WAV:
|
|
// TERROR: limit data read while rebuilding cache
|
|
{
|
|
CAudioSourceMemWave *memAs = new CAudioSourceMemWave( s_pSfx );
|
|
memAs->SetRebuildingCache();
|
|
as = memAs;
|
|
}
|
|
break;
|
|
case CAudioSource::AUDIO_SOURCE_MP3:
|
|
#if defined( MP3_SUPPORT )
|
|
as = new CAudioSourceMP3Cache( s_pSfx );
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
if ( as )
|
|
{
|
|
as->GetCacheData( this );
|
|
delete as;
|
|
}
|
|
}
|
|
|
|
#define AUDIOSOURCE_CACHE_VERSION 3
|
|
class CAudioSourceCache : public IAudioSourceCache
|
|
{
|
|
public:
|
|
typedef CUtlCachedFileData< CAudioSourceCachedInfo > CacheType_t;
|
|
|
|
CAudioSourceCache()
|
|
{
|
|
m_pMasterSoundCache = NULL;
|
|
m_pBuildingCache = NULL;
|
|
m_nServerCount = -1;
|
|
}
|
|
|
|
bool Init( unsigned int memSize );
|
|
void Shutdown();
|
|
|
|
void LevelInit( char const *mapname );
|
|
void LevelShutdown();
|
|
|
|
virtual CAudioSourceCachedInfo *GetInfo( int audiosourcetype, bool soundisprecached, CSfxTable *sfx );
|
|
virtual CAudioSourceCachedInfo *GetInfoByName( const char *soundName );
|
|
virtual void RebuildCacheEntry( int audiosourcetype, bool soundisprecached, CSfxTable *sfx );
|
|
|
|
bool BuildMasterPrecachedSoundsCache();
|
|
bool UpdateMasterPrecachedSoundsCache();
|
|
void WriteManifest();
|
|
|
|
void ValidateSoundCache( char const *pchWavFile );
|
|
private:
|
|
// Purpose:
|
|
CacheType_t *LookUpCacheEntry( const char *fn, int audiosourcetype, bool soundisprecached, CSfxTable *sfx );
|
|
|
|
struct AudioSourceUsage_t
|
|
{
|
|
AudioSourceUsage_t() :
|
|
handle( 0 ),
|
|
count( 0u )
|
|
{
|
|
}
|
|
FileNameHandle_t handle;
|
|
unsigned int count;
|
|
};
|
|
|
|
static bool AudioSourceUsageLessFunc( const AudioSourceUsage_t& lhs, const AudioSourceUsage_t& rhs )
|
|
{
|
|
return lhs.handle < rhs.handle;
|
|
}
|
|
|
|
CacheType_t *AllocAudioCache( char const *cachename, bool bNeverCheckDisk );
|
|
bool LoadMasterCache( char const *pchLanguage, bool bAllowEmpty );
|
|
|
|
void RecursiveBuildSoundList( CUtlRBTree< FileNameHandle_t, int >& other, const char *pCurrentLanguage, const char *pCurrentDir, const char *pPathID );
|
|
CacheType_t *BuildCacheFromList( char const *cachename, CUtlRBTree< FileNameHandle_t, int >& list, bool showprogress = false, float flProgressStart = 0.0f, float flProgressEnd = 1.0f );
|
|
bool IsValidCache( char const *cachename );
|
|
void RemoveCache( char const *cachename );
|
|
|
|
void GetAudioCacheLanguageSuffix( CUtlString &sLanguage );
|
|
void PrefetchCommandSounds();
|
|
|
|
enum
|
|
{
|
|
MAX_LIST_SIZE = 1024
|
|
};
|
|
|
|
CUtlString m_szMODPath;
|
|
CUtlString m_szMapCacheBase;
|
|
CUtlString m_szMasterCache;
|
|
CUtlString m_szCurrentLanguage;
|
|
|
|
typedef enum
|
|
{
|
|
CACHE_MASTER,
|
|
CACHE_BUILDING
|
|
} SoundCacheType_t;
|
|
|
|
void SetCachePointer( SoundCacheType_t ptrType, CacheType_t *ptr );
|
|
|
|
// All sounds (no startup data) referenced anywhere in game
|
|
CacheType_t *m_pMasterSoundCache;
|
|
CacheType_t *m_pBuildingCache;
|
|
|
|
int m_nServerCount;
|
|
};
|
|
|
|
static CAudioSourceCache g_ASCache;
|
|
IAudioSourceCache *audiosourcecache = &g_ASCache;
|
|
|
|
unsigned int CAudioSourceCachedInfoHandle_t::s_nCurrentFlushCount = 1;
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCachedInfoHandle_t::InvalidateCache()
|
|
{
|
|
++s_nCurrentFlushCount;
|
|
}
|
|
|
|
bool CAudioSourceCache::LoadMasterCache( char const *pchLanguage, bool bAllowEmpty )
|
|
{
|
|
m_szMasterCache = CFmtStr( "%s/_master%s.cache", AUDIOSOURCE_CACHE_ROOTDIR, pchLanguage );
|
|
|
|
char fullpath[ MAX_PATH ];
|
|
Q_snprintf( fullpath, sizeof( fullpath ), "%s%s", m_szMODPath.String(), m_szMasterCache.String() );
|
|
// Just for display
|
|
Q_FixSlashes( fullpath, INCORRECT_PATH_SEPARATOR );
|
|
Q_strlower( fullpath );
|
|
DevMsg( 1, "Trying cache : '%s'\n", fullpath );
|
|
|
|
CacheType_t *cache = AllocAudioCache( m_szMasterCache.String(), true );
|
|
|
|
Assert( cache );
|
|
if ( !cache->Init() ||
|
|
( !bAllowEmpty && cache->Count() == 0 ) )
|
|
{
|
|
Warning( "Failed to init '%s'\n", m_szMasterCache.String() );
|
|
m_szMasterCache = "";
|
|
delete cache;
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
DevMsg( 1, "Successfully loaded audio cache file\n" );
|
|
SetCachePointer( CACHE_MASTER, cache );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceCache::Init( unsigned int memSize )
|
|
{
|
|
#if defined( _DEBUG )
|
|
Msg( "CAudioSourceCache: Init\n" );
|
|
#endif
|
|
|
|
if ( !wavedatacache->Init( memSize ) )
|
|
{
|
|
Error( "Unable to init wavedatacache system\n" );
|
|
return false;
|
|
}
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 doesn't use audio source caches
|
|
return true;
|
|
}
|
|
|
|
GetAudioCacheLanguageSuffix( m_szCurrentLanguage );
|
|
|
|
if ( m_szCurrentLanguage.Length() > 0 )
|
|
{
|
|
DevMsg( 1, "Audio Caches using '%s' as suffix\n", m_szCurrentLanguage.String() );
|
|
}
|
|
|
|
char sz[ MAX_PATH ];
|
|
Q_strncpy( sz, g_pSoundServices->GetGameDir(), sizeof( sz ) );
|
|
Q_StripTrailingSlash( sz );
|
|
|
|
// Special handling for -tempcontent so that audio caches are loaded out of the tempentent "GAME" dir rather than the main "MOD" dir.
|
|
// To do this we pass in a full path so that the CUtlCachedFileData doesn't get to specify that it only wants "MOD" search path
|
|
if ( CommandLine()->FindParm( "-tempcontent" ) )
|
|
{
|
|
Q_strncat( sz, "_tempcontent", sizeof( sz ), COPY_ALL_CHARACTERS );
|
|
}
|
|
|
|
V_StripTrailingSlash( sz );
|
|
|
|
char szDLCPath[ MAX_PATH ];
|
|
|
|
int nHighestDLC = 1;
|
|
for ( ;nHighestDLC <= 99; nHighestDLC++ )
|
|
{
|
|
V_snprintf( szDLCPath, sizeof( szDLCPath ), "%s_dlc%d", sz, nHighestDLC );
|
|
if ( !g_pFullFileSystem->IsDirectory( szDLCPath ) )
|
|
{
|
|
// does not exist, highest dlc available is previous
|
|
nHighestDLC--;
|
|
break;
|
|
}
|
|
|
|
V_snprintf( szDLCPath, sizeof( szDLCPath ), "%s_dlc%d/dlc_disabled.txt", sz, nHighestDLC );
|
|
if ( g_pFullFileSystem->FileExists( szDLCPath ) )
|
|
{
|
|
// disabled, highest dlc available is previous
|
|
nHighestDLC--;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( nHighestDLC > 0 )
|
|
{
|
|
V_snprintf( szDLCPath, sizeof( szDLCPath ), "%s_dlc%d", sz, nHighestDLC );
|
|
}
|
|
else
|
|
{
|
|
V_strncpy( szDLCPath, sz, sizeof( szDLCPath ) );
|
|
}
|
|
|
|
Q_FixSlashes( szDLCPath );
|
|
Q_strlower( szDLCPath );
|
|
|
|
m_szMODPath = szDLCPath;
|
|
// Add trailing slash
|
|
m_szMODPath += CFmtStr( "%c", CORRECT_PATH_SEPARATOR );
|
|
|
|
g_pFullFileSystem->CreateDirHierarchy( CFmtStr( "%s%s", m_szMODPath.String(), AUDIOSOURCE_CACHE_ROOTDIR ), "GAME" );
|
|
|
|
// Assume failure
|
|
SetCachePointer( CACHE_MASTER, NULL );
|
|
bool bSuccess = LoadMasterCache( m_szCurrentLanguage, false );
|
|
if ( !bSuccess && Q_stricmp( m_szCurrentLanguage, "" ) )
|
|
{
|
|
bSuccess = LoadMasterCache( "", true );
|
|
}
|
|
|
|
if ( !bSuccess )
|
|
{
|
|
Warning( " .cache load failed, forcing rebuild [lang:%s]!\n", m_szCurrentLanguage.String() );
|
|
BuildMasterPrecachedSoundsCache();
|
|
}
|
|
// Tools mode always tries to update sound cache for now
|
|
else if ( CommandLine()->FindParm( "-tools" ) &&
|
|
!CommandLine()->FindParm( "-norebuildaudiocache" ) )
|
|
{
|
|
UpdateMasterPrecachedSoundsCache();
|
|
}
|
|
|
|
if ( snd_prefetch_common.GetBool() )
|
|
{
|
|
PrefetchCommandSounds();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::Shutdown()
|
|
{
|
|
#if defined( _DEBUG )
|
|
Msg( "CAudioSourceCache: Shutdown\n" );
|
|
#endif
|
|
|
|
if ( !IsGameConsole() || IsPC() )
|
|
{
|
|
if ( m_pMasterSoundCache )
|
|
{
|
|
m_pMasterSoundCache->Shutdown();
|
|
delete m_pMasterSoundCache;
|
|
}
|
|
|
|
SetCachePointer( CACHE_MASTER, NULL );
|
|
}
|
|
|
|
wavedatacache->Shutdown();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *cachename -
|
|
// Output : CacheType_t
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceCache::CacheType_t *CAudioSourceCache::AllocAudioCache( char const *cachename, bool bNeverCheckDisk )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
CacheType_t *cache = new CacheType_t( CFmtStr( "%s%s", m_szMODPath.String(), cachename ), AUDIOSOURCE_CACHE_VERSION, NULL, UTL_CACHED_FILE_USE_FILESIZE, bNeverCheckDisk );
|
|
return cache;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *mapname -
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::LevelInit( char const *mapname )
|
|
{
|
|
m_szMapCacheBase = CFmtStr( "%s/%s", AUDIOSOURCE_CACHE_ROOTDIR, mapname );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::LevelShutdown()
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return;
|
|
}
|
|
|
|
// Get precached sound count and store manifest if running with -makereslists
|
|
if ( !CommandLine()->FindParm( "-makereslists" ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
int count = g_pSoundServices->GetPrecachedSoundCount();
|
|
|
|
if ( !count )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// So that we only save this out once per level
|
|
if ( g_pSoundServices->GetServerCount() == m_nServerCount )
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_nServerCount = g_pSoundServices->GetServerCount();
|
|
|
|
WriteManifest();
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::WriteManifest()
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return;
|
|
}
|
|
|
|
int count = g_pSoundServices->GetPrecachedSoundCount();
|
|
|
|
if ( !count )
|
|
{
|
|
DevMsg( "Skipping WriteManifest, no entries in sound precache string table\n" );
|
|
return;
|
|
}
|
|
|
|
// Save manifest out to disk...
|
|
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
|
|
|
|
for ( int i = 0; i < count; ++i )
|
|
{
|
|
char const *fn = g_pSoundServices->GetPrecachedSound( i );
|
|
if ( fn && fn[ 0 ] )
|
|
{
|
|
char full[ 512 ];
|
|
Q_snprintf( full, sizeof( full ), "sound\\%s", PSkipSoundChars( fn ) );
|
|
Q_strlower( full );
|
|
Q_FixSlashes( full );
|
|
|
|
// Write to file
|
|
buf.Printf( "\"%s\"\r\n", full );
|
|
}
|
|
}
|
|
|
|
g_pFullFileSystem->CreateDirHierarchy( AUDIOSOURCE_CACHE_ROOTDIR, "MOD" );
|
|
|
|
char manifest_name[ 512 ];
|
|
Q_snprintf( manifest_name, sizeof( manifest_name ), "%s.manifest", m_szMapCacheBase.String() );
|
|
|
|
if ( g_pFullFileSystem->FileExists( manifest_name, "MOD" ) &&
|
|
!g_pFullFileSystem->IsFileWritable( manifest_name, "MOD" ) )
|
|
{
|
|
g_pFullFileSystem->SetFileWritable( manifest_name, true, "MOD" );
|
|
}
|
|
|
|
// Now write to file
|
|
FileHandle_t fh;
|
|
fh = g_pFullFileSystem->Open( manifest_name, "wb" );
|
|
if ( FILESYSTEM_INVALID_HANDLE != fh )
|
|
{
|
|
g_pFullFileSystem->Write( buf.Base(), buf.TellPut(), fh );
|
|
g_pFullFileSystem->Close( fh );
|
|
|
|
DevMsg( "WriteManifest: Persisting cache manifest '%s' (%d entries)\n", manifest_name, count );
|
|
}
|
|
else
|
|
{
|
|
Warning( "WriteManifest: Unable to persist cache manifest '%s', check file permissions\n", manifest_name );
|
|
}
|
|
}
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceCache::CacheType_t *CAudioSourceCache::LookUpCacheEntry( const char *fn, int audiosourcetype, bool soundisprecached, CSfxTable *sfx )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
// Hack to remember the type of audiosource to create if we need to recreate it
|
|
CAudioSourceCachedInfo::s_CurrentType = audiosourcetype;
|
|
CAudioSourceCachedInfo::s_pSfx = sfx;
|
|
CAudioSourceCachedInfo::s_bIsPrecacheSound = false;
|
|
|
|
CacheType_t *pCache = NULL;
|
|
|
|
// If building a cache, just shortcut through to target cache
|
|
if ( m_pBuildingCache )
|
|
{
|
|
pCache = m_pBuildingCache;
|
|
}
|
|
else
|
|
{
|
|
// Grab from the full master list
|
|
pCache = m_pMasterSoundCache;
|
|
}
|
|
|
|
return pCache;
|
|
}
|
|
|
|
|
|
CAudioSourceCachedInfo *CAudioSourceCache::GetInfoByName( const char *soundName )
|
|
{
|
|
|
|
VPROF("CAudioSourceCache::GetInfoByName");
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return NULL;
|
|
}
|
|
|
|
if ( !m_pMasterSoundCache )
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
return m_pMasterSoundCache->Get( soundName );
|
|
}
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceCachedInfo *CAudioSourceCache::GetInfo( int audiosourcetype, bool soundisprecached, CSfxTable *sfx )
|
|
{
|
|
VPROF("CAudioSourceCache::GetInfo");
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return NULL;
|
|
}
|
|
|
|
Assert( sfx );
|
|
|
|
char fn[ 512 ];
|
|
char nameBuf[MAX_PATH];
|
|
Q_snprintf( fn, sizeof( fn ), "sound/%s", sfx->GetFileName( nameBuf, sizeof(nameBuf) ) );
|
|
|
|
CAudioSourceCachedInfo *info = NULL;
|
|
CacheType_t *pCache = LookUpCacheEntry( fn, audiosourcetype, soundisprecached, sfx );
|
|
if ( !pCache )
|
|
return NULL;
|
|
|
|
info = pCache->Get( fn );
|
|
|
|
if ( info && info->Format() == 0 )
|
|
{
|
|
if ( g_pFullFileSystem->FileExists( fn, "BSP" ) )
|
|
{
|
|
DevMsg( 1, "Forced rebuild of bsp cache sound '%s'\n", fn );
|
|
info = pCache->RebuildItem( fn );
|
|
Assert( info->Format() != 0 );
|
|
}
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
|
|
void CAudioSourceCache::RebuildCacheEntry( int audiosourcetype, bool soundisprecached, CSfxTable *sfx )
|
|
{
|
|
VPROF("CAudioSourceCache::GetInfo");
|
|
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return;
|
|
}
|
|
|
|
Assert( sfx );
|
|
|
|
char fn[ 512 ];
|
|
char nameBuf[MAX_PATH];
|
|
Q_snprintf( fn, sizeof( fn ), "sound/%s", sfx->GetFileName( nameBuf, sizeof(nameBuf) ) );
|
|
CacheType_t *pCache = LookUpCacheEntry( fn, audiosourcetype, soundisprecached, sfx );
|
|
if ( !pCache )
|
|
return;
|
|
|
|
pCache->RebuildItem( fn );
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *cachename -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceCache::IsValidCache( char const *cachename )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CacheType_t *cache = AllocAudioCache( cachename, true );
|
|
|
|
// This will delete any outdated .cache files
|
|
bool valid = cache->IsUpToDate();
|
|
|
|
delete cache;
|
|
|
|
return valid;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *cachename -
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::RemoveCache( char const *cachename )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( g_pFullFileSystem->FileExists( cachename, "MOD" ) )
|
|
{
|
|
if ( !g_pFullFileSystem->IsFileWritable( cachename, "MOD" ) )
|
|
{
|
|
g_pFullFileSystem->SetFileWritable( cachename, true, "MOD" );
|
|
}
|
|
g_pFullFileSystem->RemoveFile( cachename, "MOD" );
|
|
}
|
|
}
|
|
|
|
void CAudioSourceCache::SetCachePointer( SoundCacheType_t ptrType, CacheType_t *ptr )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool dirty = false;
|
|
|
|
switch ( ptrType )
|
|
{
|
|
default:
|
|
Error( "SetCachePointer with bogus type %i\n", (int)ptrType );
|
|
break;
|
|
case CACHE_MASTER:
|
|
if ( m_pMasterSoundCache != ptr )
|
|
{
|
|
dirty = true;
|
|
m_pMasterSoundCache = ptr;
|
|
}
|
|
break;
|
|
case CACHE_BUILDING:
|
|
if ( m_pBuildingCache != ptr )
|
|
{
|
|
dirty = true;
|
|
m_pBuildingCache = ptr;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( dirty )
|
|
{
|
|
CAudioSourceCachedInfoHandle_t::InvalidateCache();
|
|
}
|
|
}
|
|
|
|
void CAudioSourceCache::ValidateSoundCache( char const *pchWavFile )
|
|
{
|
|
Assert( m_pMasterSoundCache );
|
|
if ( !m_pMasterSoundCache )
|
|
return;
|
|
|
|
m_pMasterSoundCache->SetNeverCheckDisk( false );
|
|
|
|
// Touch the cache
|
|
CSfxTable *pTable = S_PrecacheSound( &pchWavFile[ SOUND_DIRECTORY_LENGTH ] );
|
|
if ( pTable && pTable->pSource )
|
|
{
|
|
CAudioSourceCachedInfo::s_CurrentType = pTable->pSource->GetType();
|
|
CAudioSourceCachedInfo::s_pSfx = pTable;
|
|
CAudioSourceCachedInfo::s_bIsPrecacheSound = false;
|
|
|
|
g_bSoundRebuilt = false;
|
|
|
|
m_pMasterSoundCache->RecheckItem( pchWavFile );
|
|
|
|
if ( g_bSoundRebuilt )
|
|
{
|
|
Msg( " updated '%s'\n", (char *)&pchWavFile[ SOUND_DIRECTORY_LENGTH ] );
|
|
}
|
|
}
|
|
|
|
m_pMasterSoundCache->SetNeverCheckDisk( true );
|
|
|
|
// Persist data to HD if dirty
|
|
if ( m_pMasterSoundCache->IsDirty() )
|
|
{
|
|
m_pMasterSoundCache->Save();
|
|
}
|
|
}
|
|
|
|
bool CAudioSourceCache::UpdateMasterPrecachedSoundsCache()
|
|
{
|
|
Assert( m_pMasterSoundCache );
|
|
if ( !m_pMasterSoundCache )
|
|
return true;
|
|
|
|
float flStart = Plat_FloatTime();
|
|
|
|
Msg( "Updating sound cache [%d entries]\n", m_pMasterSoundCache->Count() );
|
|
|
|
CUtlRBTree< FileNameHandle_t, int > soundsOnDisk( 0, 0, DefLessFunc( FileNameHandle_t ) );
|
|
// Build recursive list of all wav files for all languages
|
|
RecursiveBuildSoundList( soundsOnDisk, m_szCurrentLanguage, "sound", "GAME" );
|
|
|
|
Msg( "Found %d sound files on disk\n", soundsOnDisk.Count() );
|
|
|
|
char soundname[ 512 ];
|
|
|
|
m_pMasterSoundCache->SetNeverCheckDisk( false );
|
|
m_pMasterSoundCache->ForceRecheckDiskInfo();
|
|
|
|
int nUpdated = 0;
|
|
|
|
for ( int i = soundsOnDisk.FirstInorder();
|
|
i != soundsOnDisk.InvalidIndex();
|
|
i = soundsOnDisk.NextInorder( i ) )
|
|
{
|
|
FileNameHandle_t& handle = soundsOnDisk[ i ];
|
|
soundname[ 0 ] = 0;
|
|
g_pFullFileSystem->String( handle, soundname, sizeof( soundname ) );
|
|
|
|
g_bSoundRebuilt = false;
|
|
|
|
// Touch the cache
|
|
CSfxTable *pTable = S_PrecacheSound( &soundname[ SOUND_DIRECTORY_LENGTH ] );
|
|
if ( pTable && pTable->pSource )
|
|
{
|
|
CAudioSourceCachedInfo::s_CurrentType = pTable->pSource->GetType();
|
|
CAudioSourceCachedInfo::s_pSfx = pTable;
|
|
CAudioSourceCachedInfo::s_bIsPrecacheSound = false;
|
|
|
|
m_pMasterSoundCache->Get( soundname );
|
|
}
|
|
|
|
if ( g_bSoundRebuilt )
|
|
{
|
|
++nUpdated;
|
|
Msg( " updated '%s'\n", (char *)&soundname[ SOUND_DIRECTORY_LENGTH ] );
|
|
}
|
|
}
|
|
|
|
m_pMasterSoundCache->SetNeverCheckDisk( true );
|
|
|
|
// Persist data to HD if dirty
|
|
m_pMasterSoundCache->Save();
|
|
|
|
float flEnd = Plat_FloatTime();
|
|
|
|
Msg( "Updated %i out of %i cached files [%.3f msec]\n", nUpdated, soundsOnDisk.Count(), 1000.0f * ( flEnd - flStart ) );
|
|
|
|
CAudioSourceCachedInfoHandle_t::InvalidateCache();
|
|
|
|
return true;
|
|
}
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : showprogress -
|
|
// Output : Returns true on success, false on failure.
|
|
//-----------------------------------------------------------------------------
|
|
bool CAudioSourceCache::BuildMasterPrecachedSoundsCache()
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return false;
|
|
}
|
|
|
|
char fn[ 512 ];
|
|
Q_snprintf( fn, sizeof( fn ), "%s/%s%s.cache", AUDIOSOURCE_CACHE_ROOTDIR, MASTER_CACHE_NAME, m_szCurrentLanguage.String() );
|
|
|
|
DevMsg( 1, "Fast Build Temp Cache: '%s'\n", fn );
|
|
|
|
// Blow away the cache if rebuilding, which will force a full cache build
|
|
RemoveCache( fn );
|
|
|
|
g_pSoundServices->CacheBuildingStart();
|
|
|
|
CacheType_t *pOtherNoData = NULL;
|
|
CUtlRBTree< FileNameHandle_t, int > other( 0, 0, DefLessFunc( FileNameHandle_t ) );
|
|
|
|
// Build recursive list of all wav files for all languages
|
|
RecursiveBuildSoundList( other, m_szCurrentLanguage, "sound", "GAME" );
|
|
|
|
pOtherNoData = BuildCacheFromList( fn, other, true, 0.0f, 1.0f );
|
|
if ( pOtherNoData )
|
|
{
|
|
if ( m_pMasterSoundCache )
|
|
{
|
|
// Don't shutdown/save, since we have a new one already
|
|
delete m_pMasterSoundCache;
|
|
}
|
|
|
|
// Take over ptr
|
|
SetCachePointer( CACHE_MASTER, pOtherNoData );
|
|
}
|
|
|
|
g_pSoundServices->CacheBuildingFinish();
|
|
|
|
return true;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::RecursiveBuildSoundList( CUtlRBTree< FileNameHandle_t, int >& root, const char *pLanguage, const char *pCurrentDir, const char *pathID )
|
|
{
|
|
FileFindHandle_t fh;
|
|
FileNameHandle_t handle;
|
|
|
|
char path[ 512 ];
|
|
Q_snprintf( path, sizeof( path ), "%s/*.*", pCurrentDir );
|
|
|
|
Q_FixSlashes( path );
|
|
|
|
char const *fn = g_pFullFileSystem->FindFirstEx( path, pathID, &fh );
|
|
if ( fn )
|
|
{
|
|
do
|
|
{
|
|
if ( *fn != '.' )
|
|
{
|
|
if ( g_pFullFileSystem->FindIsDirectory( fh ) )
|
|
{
|
|
char nextdir[ 512 ];
|
|
Q_snprintf( nextdir, sizeof( nextdir ), "%s/%s", pCurrentDir, fn );
|
|
|
|
RecursiveBuildSoundList( root, pLanguage, nextdir, pathID );
|
|
}
|
|
else
|
|
{
|
|
char ext[ 10 ];
|
|
Q_ExtractFileExtension( fn, ext, sizeof( ext ) );
|
|
|
|
if ( ( !Q_stricmp( ext, "wav" ) || !Q_stricmp( ext, "mp3" ) ) && !Q_stristr( fn, ".360." ) && !Q_stristr( fn, ".ps3." ) )
|
|
{
|
|
char relative[ 512 ];
|
|
Q_snprintf( relative, sizeof( relative ), "%s/%s", pCurrentDir, fn );
|
|
|
|
Q_FixSlashes( relative );
|
|
handle = g_pFullFileSystem->FindOrAddFileName( relative );
|
|
|
|
if ( root.Find( handle ) == root.InvalidIndex() )
|
|
{
|
|
root.Insert( handle );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn = g_pFullFileSystem->FindNext( fh );
|
|
|
|
} while ( fn );
|
|
|
|
g_pFullFileSystem->FindClose( fh );
|
|
}
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Purpose:
|
|
// Input : *cachename -
|
|
// FileNameHandle_t -
|
|
// soundlist -
|
|
// fulldata -
|
|
// showprogress -
|
|
// Output : CAudioSourceCache::CacheType_t
|
|
//-----------------------------------------------------------------------------
|
|
CAudioSourceCache::CacheType_t *CAudioSourceCache::BuildCacheFromList( char const *cachename, CUtlRBTree< FileNameHandle_t, int >& soundlist, bool showprogress /*= false*/, float flProgressStart /*= 0.0f*/, float flProgressEnd /*= 1.0f*/ )
|
|
{
|
|
if ( IsGameConsole() )
|
|
{
|
|
// 360 not using
|
|
return NULL;
|
|
}
|
|
|
|
float flStart = Plat_FloatTime();
|
|
|
|
|
|
CacheType_t *newCache = NULL;
|
|
|
|
newCache = AllocAudioCache( cachename, false );
|
|
Assert( newCache );
|
|
if ( newCache->Init() )
|
|
{
|
|
SetCachePointer( CACHE_BUILDING, newCache );
|
|
|
|
int visited = 0;
|
|
|
|
for ( int i = soundlist.FirstInorder(); i != soundlist.InvalidIndex(); i = soundlist.NextInorder( i ) )
|
|
{
|
|
FileNameHandle_t& handle = soundlist[ i ];
|
|
char soundname[ 512 ];
|
|
soundname[ 0 ] = 0;
|
|
if ( g_pFullFileSystem->String( handle, soundname, sizeof( soundname ) ) )
|
|
{
|
|
// Touch the cache
|
|
// Force it to go into the "other" cache but to also appear as "full data" precache
|
|
CSfxTable *pTable = S_PrecacheSound( &soundname[ SOUND_DIRECTORY_LENGTH ] );
|
|
// This will "re-cache" this if it's not in this level's cache already
|
|
if ( pTable && pTable->pSource )
|
|
{
|
|
GetInfo( pTable->pSource->GetType(), false, pTable );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Assert( !"Unable to find FileNameHandle_t in fileystem list." );
|
|
}
|
|
|
|
++visited;
|
|
|
|
if ( !( visited % 100 ) )
|
|
{
|
|
Msg( " progress %i/%i (%i %%)\n",
|
|
visited, soundlist.Count(), (int)( 100.0f * ( float) visited / (float) soundlist.Count() ) );
|
|
}
|
|
|
|
if ( showprogress )
|
|
{
|
|
float frac = ( float )( visited - 1 )/( float )soundlist.Count();
|
|
|
|
frac = flProgressStart + frac * ( flProgressEnd - flProgressStart );
|
|
|
|
char base[ 256 ];
|
|
Q_FileBase( soundname, base, sizeof( base ) );
|
|
Q_strlower( base );
|
|
g_pSoundServices->CacheBuildingUpdateProgress( frac, base );
|
|
}
|
|
}
|
|
|
|
Msg( "Touched %i cached files\n", soundlist.Count() );
|
|
|
|
SetCachePointer( CACHE_BUILDING, NULL );
|
|
|
|
// Persist data to HD if dirty
|
|
newCache->Save();
|
|
}
|
|
else
|
|
{
|
|
delete newCache;
|
|
newCache = NULL;
|
|
}
|
|
|
|
float flEnd = Plat_FloatTime();
|
|
Msg( "Elapsed time: %.2f seconds\n",
|
|
flEnd - flStart );
|
|
|
|
return newCache;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
void CAudioSourceCache::GetAudioCacheLanguageSuffix( CUtlString &sLanguage )
|
|
{
|
|
char const *pchLanguage = g_pSoundServices->GetUILanguage();
|
|
|
|
if ( !pchLanguage || !*pchLanguage || !Q_stricmp( pchLanguage, "english" ) )
|
|
{
|
|
sLanguage = "";
|
|
return;
|
|
}
|
|
|
|
//Check language right here to see if we need the caches for it.
|
|
char szLanguageList[ MAX_PATH ];
|
|
Q_snprintf( szLanguageList, sizeof( szLanguageList ), "%s/localization_cache_list.txt", AUDIOSOURCE_CACHE_ROOTDIR );
|
|
FileHandle_t fh = g_pFullFileSystem->Open( szLanguageList, "r" );
|
|
char szCacheLanguage[ MAX_LIST_SIZE ];
|
|
if ( fh )
|
|
{
|
|
g_pFullFileSystem->Read( szCacheLanguage, MAX_LIST_SIZE, fh);
|
|
g_pFullFileSystem->Close( fh );
|
|
|
|
if ( Q_stristr( szCacheLanguage, pchLanguage ) )
|
|
{
|
|
sLanguage = CFmtStr( "_%s", pchLanguage );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
sLanguage = "";
|
|
return;
|
|
}
|
|
}
|
|
|
|
sLanguage = CFmtStr( "_%s", pchLanguage );
|
|
return;
|
|
}
|
|
|
|
void CAudioSourceCache::PrefetchCommandSounds()
|
|
{
|
|
if ( IsGameConsole() )
|
|
return;
|
|
|
|
if ( !m_pMasterSoundCache )
|
|
return;
|
|
|
|
CUtlVector< CUtlString > vecSearch;
|
|
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
|
|
if ( g_pFullFileSystem->ReadFile( SOUND_PREFETCH_FILE, "GAME", buf ) )
|
|
{
|
|
characterset_t breakSet;
|
|
CharacterSetBuild( &breakSet, "" );
|
|
|
|
// parse reslist
|
|
char substr[MAX_PATH];
|
|
for ( ;; )
|
|
{
|
|
int nTokenSize = buf.ParseToken( &breakSet, substr, sizeof( substr ), true );
|
|
if ( nTokenSize <= 0 )
|
|
{
|
|
break;
|
|
}
|
|
|
|
Q_FixSlashes( substr );
|
|
Q_strlower( substr );
|
|
vecSearch.AddToTail( CUtlString( substr ) );
|
|
|
|
Msg( "Prefetching data for subdir: %s\n", substr );
|
|
}
|
|
}
|
|
|
|
// Nothing to do
|
|
if ( !vecSearch.Count() )
|
|
return;
|
|
|
|
COM_TimestampedLog( "PrefetchSounds Start" );
|
|
float flStart = Plat_FloatTime();
|
|
uint64 uBytesPrefeteched = 0ull;
|
|
uint32 uSoundsPrefetched = 0u;
|
|
// Now walk the cache and prefetch shiz
|
|
for ( int i = 0 ; i < m_pMasterSoundCache->Count(); ++i )
|
|
{
|
|
char szFile[ MAX_PATH ];
|
|
m_pMasterSoundCache->GetElementName( i, szFile, sizeof( szFile ) );
|
|
Q_FixSlashes( szFile );
|
|
Q_strlower( szFile );
|
|
|
|
for ( int j = 0; j < vecSearch.Count(); ++j )
|
|
{
|
|
const CUtlString &str = vecSearch[ j ];
|
|
if ( Q_stristr( szFile, str ) )
|
|
{
|
|
++uSoundsPrefetched;
|
|
const CAudioSourceCachedInfo *info = (*m_pMasterSoundCache)[ i ];
|
|
uBytesPrefeteched += info->DataSize();
|
|
|
|
S_PrefetchSound( szFile, false );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
float flEnd = Plat_FloatTime();
|
|
|
|
COM_TimestampedLog( "PrefetchSounds Finish" );
|
|
Msg( "Prefetched %u sounds, %s [%.3f msec]\n",
|
|
uSoundsPrefetched, Q_pretifymem( uBytesPrefeteched ), 1000.0f * ( flEnd - flStart ) );
|
|
}
|
|
|
|
CON_COMMAND( snd_rebuildaudiocache, "rebuild audio cache for current language\n" )
|
|
{
|
|
g_ASCache.BuildMasterPrecachedSoundsCache();
|
|
}
|
|
|
|
CON_COMMAND( snd_writemanifest, "If running a game, outputs the precache manifest for the current level\n" )
|
|
{
|
|
g_ASCache.WriteManifest();
|
|
}
|
|
|
|
CON_COMMAND( snd_updateaudiocache, "checks _master.cache based on file sizes and rebuilds any change/new entries\n" )
|
|
{
|
|
g_ASCache.UpdateMasterPrecachedSoundsCache();
|
|
}
|
|
|
|
void S_ValidateSoundCache( char const *pchWavFile )
|
|
{
|
|
g_ASCache.ValidateSoundCache( pchWavFile );
|
|
}
|