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.
602 lines
16 KiB
602 lines
16 KiB
//========= Copyright Valve Corporation, All rights reserved. ============// |
|
// |
|
// Purpose: |
|
// |
|
// $NoKeywords: $ |
|
// |
|
//=============================================================================// |
|
#include "audio_pch.h" |
|
#include "snd_mp3_source.h" |
|
#include "snd_dma.h" |
|
#include "snd_wave_mixer_mp3.h" |
|
#include "filesystem_engine.h" |
|
#include "utldict.h" |
|
|
|
// memdbgon must be the last include file in a .cpp file!!! |
|
#include "tier0/memdbgon.h" |
|
|
|
#ifndef DEDICATED // have to test this because VPC is forcing us to compile this file. |
|
|
|
// How many bytes initial data bytes of the mp3 should be saved in the soundcache, in addition to the small amount of |
|
// metadata (playbackrate, etc). This will increase memory usage by |
|
// ( N * number-of-precached-mp3-sounds-in-the-whole-game ) at all times, as the soundcache is held in memory. |
|
// |
|
// Right now we're setting this to zero. The IsReadyToMix() logic at the data layer will delay mixing of the sound until |
|
// it arrives. Setting this to anything above zero, however, will allow the sound to start, so it needs to either be |
|
// enough to cover SND_ASYNC_LOOKAHEAD_SECONDS or none at all. |
|
#define MP3_STARTUP_DATA_SIZE_BYTES 0 |
|
|
|
CUtlDict< CSentence *, int> g_PhonemeFileSentences; |
|
bool g_bAllPhonemesLoaded; |
|
|
|
void PhonemeMP3Shutdown( void ) |
|
{ |
|
g_PhonemeFileSentences.PurgeAndDeleteElements(); |
|
g_bAllPhonemesLoaded = false; |
|
} |
|
|
|
void AddPhonemesFromFile( const char *pszFileName ) |
|
{ |
|
// If all Phonemes are loaded, do not load anymore |
|
if ( g_bAllPhonemesLoaded && g_PhonemeFileSentences.Count() != 0 ) |
|
return; |
|
|
|
// Empty file name implies stop loading more phonemes |
|
if ( pszFileName == NULL ) |
|
{ |
|
g_bAllPhonemesLoaded = true; |
|
return; |
|
} |
|
|
|
// Load this file |
|
g_bAllPhonemesLoaded = false; |
|
|
|
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); |
|
if ( g_pFileSystem->ReadFile( pszFileName, "MOD", buf ) ) |
|
{ |
|
while ( 1 ) |
|
{ |
|
char token[4096]; |
|
buf.GetString( token ); |
|
|
|
V_FixSlashes( token ); |
|
|
|
int iIndex = g_PhonemeFileSentences.Find( token ); |
|
|
|
if ( iIndex != g_PhonemeFileSentences.InvalidIndex() ) |
|
{ |
|
delete g_PhonemeFileSentences.Element( iIndex ); |
|
g_PhonemeFileSentences.Remove( token ); |
|
} |
|
|
|
CSentence *pSentence = new CSentence; |
|
g_PhonemeFileSentences.Insert( token, pSentence ); |
|
|
|
buf.GetString( token ); |
|
|
|
if ( strlen( token ) <= 0 ) |
|
break; |
|
|
|
if ( !stricmp( token, "{" ) ) |
|
{ |
|
pSentence->InitFromBuffer( buf ); |
|
} |
|
} |
|
} |
|
} |
|
|
|
CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx ) |
|
{ |
|
m_sampleRate = 0; |
|
m_pSfx = pSfx; |
|
m_refCount = 0; |
|
|
|
m_dataStart = 0; |
|
|
|
intp file = g_pSndIO->open( pSfx->GetFileName() ); |
|
if ( file != -1 ) |
|
{ |
|
m_dataSize = g_pSndIO->size( file ); |
|
g_pSndIO->close( file ); |
|
} |
|
else |
|
{ |
|
// No sound cache, the file isn't here, print this so that the relatively deep failure points that are about to |
|
// spew make a little more sense |
|
Warning( "MP3 is completely missing, sound system will be upset to learn of this [ %s ]\n", pSfx->GetFileName() ); |
|
m_dataSize = 0; |
|
} |
|
|
|
|
|
m_nCachedDataSize = 0; |
|
m_bIsPlayOnce = false; |
|
m_bIsSentenceWord = false; |
|
m_bCheckedForPendingSentence = false; |
|
} |
|
|
|
CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) |
|
{ |
|
m_pSfx = pSfx; |
|
m_refCount = 0; |
|
|
|
m_sampleRate = info->SampleRate(); |
|
m_dataSize = info->DataSize(); |
|
m_dataStart = info->DataStart(); |
|
|
|
m_nCachedDataSize = 0; |
|
m_bIsPlayOnce = false; |
|
m_bCheckedForPendingSentence = false; |
|
|
|
CheckAudioSourceCache(); |
|
} |
|
|
|
CAudioSourceMP3::~CAudioSourceMP3() |
|
{ |
|
} |
|
|
|
// mixer's references |
|
void CAudioSourceMP3::ReferenceAdd( CAudioMixer * ) |
|
{ |
|
m_refCount++; |
|
} |
|
|
|
void CAudioSourceMP3::ReferenceRemove( CAudioMixer * ) |
|
{ |
|
m_refCount--; |
|
if ( m_refCount == 0 && IsPlayOnce() ) |
|
{ |
|
SetPlayOnce( false ); // in case it gets used again |
|
CacheUnload(); |
|
} |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : Returns true on success, false on failure. |
|
//----------------------------------------------------------------------------- |
|
bool CAudioSourceMP3::IsAsyncLoad() |
|
{ |
|
// 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) |
|
return ( m_nCachedDataSize > 0 ) ? false : true; |
|
} |
|
|
|
// check reference count, return true if nothing is referencing this |
|
bool CAudioSourceMP3::CanDelete( void ) |
|
{ |
|
return m_refCount > 0 ? false : true; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : int |
|
//----------------------------------------------------------------------------- |
|
int CAudioSourceMP3::GetType() |
|
{ |
|
return AUDIO_SOURCE_MP3; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
void CAudioSourceMP3::SetSentence( CSentence *pSentence ) |
|
{ |
|
CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet(); |
|
|
|
if ( !info ) |
|
return; |
|
|
|
if ( info && info->Sentence() ) |
|
return; |
|
|
|
CSentence *pNewSentence = new CSentence; |
|
|
|
pNewSentence->Append( 0.0f, *pSentence ); |
|
pNewSentence->MakeRuntimeOnly(); |
|
|
|
info->SetSentence( pNewSentence ); |
|
} |
|
|
|
int CAudioSourceMP3::SampleRate() |
|
{ |
|
if ( !m_sampleRate ) |
|
{ |
|
// This should've come from the sound cache. We can avoid sync I/O jank if and only if we've started streaming |
|
// data already for some other reason. (Despite the name, CreateWaveDataMemory is just creating a wrapper class |
|
// that manages access to the wave data cache) |
|
IWaveData *pData = CreateWaveDataMemory( *this ); |
|
if ( !pData->IsReadyToMix() && SND_IsInGame() ) |
|
{ |
|
// If you hit this, you're creating a sound source that isn't in the sound cache, and asking for its sample |
|
// rate before it has streamed enough data in to read it from the underlying file. Your options are: |
|
// - Rebuild sound cache or figure out why this sound wasn't included. |
|
// - Precache this sound at level load so this doesn't happen during gameplay. |
|
// - Somehow call CacheLoad() on this source earlier so it has time to get data into memory so the data |
|
// shows up as IsReadyToMix here, and this crutch won't jank. |
|
Warning( "MP3 initialized with no sound cache, this may cause janking. [ %s ]\n", GetFileName() ); |
|
// The code below will still go fine, but the mixer will emit a jank warning that the data wasn't ready and |
|
// do sync I/O |
|
} |
|
CAudioMixerWaveMP3 *pMixer = new CAudioMixerWaveMP3( pData ); |
|
m_sampleRate = pMixer->GetStreamOutputRate(); |
|
// pData ownership is passed to, and free'd by, pMixer |
|
delete pMixer; |
|
} |
|
return m_sampleRate; |
|
} |
|
|
|
void CAudioSourceMP3::GetCacheData( CAudioSourceCachedInfo *info ) |
|
{ |
|
// Don't want to replicate our cached sample rate back into the new cache, ensure we recompute it. |
|
CAudioMixerWaveMP3 *pTempMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) ); |
|
m_sampleRate = pTempMixer->GetStreamOutputRate(); |
|
delete pTempMixer; |
|
|
|
AssertMsg( m_sampleRate, "Creating cache with invalid sample rate data" ); |
|
if ( !m_sampleRate ) |
|
{ |
|
Warning( "Failed to find sample rate creating cache data for MP3, cache will be invalid [ %s ]\n", GetFileName() ); |
|
} |
|
|
|
info->SetSampleRate( m_sampleRate ); |
|
info->SetDataStart( 0 ); |
|
|
|
intp file = g_pSndIO->open( m_pSfx->GetFileName() ); |
|
if ( !file ) |
|
{ |
|
Warning( "Failed to find file for building soundcache [ %s ]\n", m_pSfx->GetFileName() ); |
|
// Don't re-use old cached value |
|
m_dataSize = 0; |
|
} |
|
else |
|
{ |
|
m_dataSize = (int)g_pSndIO->size( file ); |
|
} |
|
|
|
Assert( m_dataSize > 0 ); |
|
|
|
// Do we need to actually load any audio data? |
|
#if MP3_STARTUP_DATA_SIZE_BYTES > 0 // We may have defined the startup data to nothingness |
|
if ( info->s_bIsPrecacheSound && m_dataSize > 0 ) |
|
{ |
|
// Ideally this would mimic the wave startup data code and figure out this calculation: |
|
// int bytesNeeded = m_channels * ( m_bits >> 3 ) * m_rate * SND_ASYNC_LOOKAHEAD_SECONDS; |
|
// (plus header) |
|
int dataSize = min( MP3_STARTUP_DATA_SIZE_BYTES, m_dataSize ); |
|
byte *data = new byte[ dataSize ](); |
|
int readSize = g_pSndIO->read( data, dataSize, file ); |
|
if ( readSize != dataSize ) |
|
{ |
|
Warning( "Building soundcache, expected %i bytes of data but got %i [ %s ]\n", dataSize, readSize, m_pSfx->GetFileName() ); |
|
dataSize = readSize; |
|
} |
|
info->SetCachedDataSize( dataSize ); |
|
info->SetCachedData( data ); |
|
} |
|
#endif // MP3_STARTUP_DATA_SIZE_BYTES > 0 |
|
|
|
g_pSndIO->close( file ); |
|
|
|
// Data size gets computed in GetStartupData!!! |
|
info->SetDataSize( m_dataSize ); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
// Output : char const |
|
//----------------------------------------------------------------------------- |
|
char const *CAudioSourceMP3::GetFileName() |
|
{ |
|
return m_pSfx ? m_pSfx->GetFileName() : "NULL m_pSfx"; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAudioSourceMP3::CheckAudioSourceCache() |
|
{ |
|
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: NULL the wave data pointer (we haven't loaded yet) |
|
//----------------------------------------------------------------------------- |
|
CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx ) : |
|
CAudioSourceMP3( pSfx ) |
|
{ |
|
m_hCache = 0; |
|
} |
|
|
|
CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : |
|
CAudioSourceMP3( pSfx, info ) |
|
{ |
|
m_hCache = 0; |
|
|
|
m_dataSize = info->DataSize(); |
|
m_dataStart = info->DataStart(); |
|
|
|
m_bNoSentence = false; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: Free any wave data we've allocated |
|
//----------------------------------------------------------------------------- |
|
CAudioSourceMP3Cache::~CAudioSourceMP3Cache( void ) |
|
{ |
|
CacheUnload(); |
|
} |
|
|
|
int CAudioSourceMP3Cache::GetCacheStatus( void ) |
|
{ |
|
bool bCacheValid; |
|
int loaded = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ) ? AUDIO_IS_LOADED : AUDIO_NOT_LOADED; |
|
if ( !bCacheValid ) |
|
{ |
|
wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(), m_dataSize, m_dataStart ); |
|
} |
|
return loaded; |
|
} |
|
|
|
|
|
void CAudioSourceMP3Cache::CacheLoad( void ) |
|
{ |
|
// Commence lazy load? |
|
if ( m_hCache != 0 ) |
|
{ |
|
GetCacheStatus(); |
|
return; |
|
} |
|
|
|
m_hCache = wavedatacache->AsyncLoadCache( m_pSfx->GetFileName(), m_dataSize, m_dataStart ); |
|
} |
|
|
|
void CAudioSourceMP3Cache::CacheUnload( void ) |
|
{ |
|
if ( m_hCache != 0 ) |
|
{ |
|
wavedatacache->Unload( m_hCache ); |
|
} |
|
} |
|
|
|
char *CAudioSourceMP3Cache::GetDataPointer( void ) |
|
{ |
|
char *pMP3Data = NULL; |
|
bool dummy = false; |
|
|
|
if ( m_hCache == 0 ) |
|
{ |
|
CacheLoad(); |
|
} |
|
|
|
wavedatacache->GetDataPointer( |
|
m_hCache, |
|
m_pSfx->GetFileName(), |
|
m_dataSize, |
|
m_dataStart, |
|
(void **)&pMP3Data, |
|
0, |
|
&dummy ); |
|
|
|
return pMP3Data; |
|
} |
|
|
|
int CAudioSourceMP3Cache::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) |
|
{ |
|
// how many bytes are available ? |
|
int totalSampleCount = m_dataSize - 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; |
|
|
|
// 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 |
|
{ |
|
// 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_nCachedDataSize ) |
|
{ |
|
sampleCount = m_nCachedDataSize - samplePosition; |
|
} |
|
|
|
// Point at preloaded/cached data from .cache file for now |
|
*pData = GetCachedDataPointer(); |
|
} |
|
|
|
if ( *pData ) |
|
{ |
|
*pData = (char *)*pData + samplePosition; |
|
} |
|
else |
|
{ |
|
// Out of data or file i/o problem |
|
sampleCount = 0; |
|
} |
|
} |
|
|
|
return sampleCount; |
|
} |
|
|
|
CAudioMixer *CAudioSourceMP3Cache::CreateMixer( int initialStreamPosition ) |
|
{ |
|
CAudioMixer *pMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) ); |
|
|
|
return pMixer; |
|
} |
|
|
|
CSentence *CAudioSourceMP3Cache::GetSentence( void ) |
|
{ |
|
// 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 ) |
|
{ |
|
if ( !m_bCheckedForPendingSentence ) |
|
{ |
|
int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() ); |
|
if ( iSentence != g_PhonemeFileSentences.InvalidIndex() ) |
|
{ |
|
sentence = g_PhonemeFileSentences.Element( iSentence ); |
|
SetSentence( sentence ); |
|
} |
|
m_bCheckedForPendingSentence = true; |
|
} |
|
} |
|
|
|
if ( !sentence ) |
|
{ |
|
m_bNoSentence = true; |
|
return NULL; |
|
} |
|
|
|
if ( sentence->m_bIsValid ) |
|
{ |
|
return sentence; |
|
} |
|
|
|
m_bNoSentence = true; |
|
|
|
return NULL; |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// CAudioSourceStreamMP3 |
|
//----------------------------------------------------------------------------- |
|
CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx ) : |
|
CAudioSourceMP3( pSfx ) |
|
{ |
|
} |
|
|
|
CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : |
|
CAudioSourceMP3( pSfx, info ) |
|
{ |
|
m_dataSize = info->DataSize(); |
|
} |
|
|
|
//----------------------------------------------------------------------------- |
|
// Purpose: |
|
//----------------------------------------------------------------------------- |
|
void CAudioSourceStreamMP3::Prefetch() |
|
{ |
|
PrefetchDataStream( m_pSfx->GetFileName(), 0, m_dataSize ); |
|
} |
|
|
|
CAudioMixer *CAudioSourceStreamMP3::CreateMixer( int intialStreamPosition ) |
|
{ |
|
// BUGBUG: Source constructs the IWaveData, mixer frees it, fix this? |
|
IWaveData *pWaveData = CreateWaveDataStream( *this, static_cast<IWaveStreamSource *>( this ), m_pSfx->GetFileName(), 0, m_dataSize, m_pSfx, 0 ); |
|
if ( pWaveData ) |
|
{ |
|
CAudioMixer *pMixer = new CAudioMixerWaveMP3( pWaveData ); |
|
if ( pMixer ) |
|
{ |
|
if ( !m_bCheckedForPendingSentence ) |
|
{ |
|
int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() ); |
|
|
|
if ( iSentence != g_PhonemeFileSentences.InvalidIndex() ) |
|
{ |
|
SetSentence( g_PhonemeFileSentences.Element( iSentence ) ); |
|
} |
|
|
|
m_bCheckedForPendingSentence = true; |
|
} |
|
|
|
return pMixer; |
|
} |
|
|
|
// no mixer but pWaveData was deleted in mixer's destructor |
|
// so no need to delete |
|
} |
|
|
|
return NULL; |
|
} |
|
|
|
int CAudioSourceStreamMP3::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) |
|
{ |
|
return 0; |
|
} |
|
|
|
bool Audio_IsMP3( const char *pName ) |
|
{ |
|
int len = strlen(pName); |
|
if ( len > 4 ) |
|
{ |
|
if ( !Q_strnicmp( &pName[len - 4], ".mp3", 4 ) ) |
|
{ |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
|
|
CAudioSource *Audio_CreateStreamedMP3( CSfxTable *pSfx ) |
|
{ |
|
CAudioSourceStreamMP3 *pMP3 = NULL; |
|
CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx ); |
|
if ( info ) |
|
{ |
|
pMP3 = new CAudioSourceStreamMP3( pSfx, info ); |
|
} |
|
else |
|
{ |
|
pMP3 = new CAudioSourceStreamMP3( pSfx ); |
|
} |
|
return pMP3; |
|
} |
|
|
|
|
|
CAudioSource *Audio_CreateMemoryMP3( CSfxTable *pSfx ) |
|
{ |
|
CAudioSourceMP3Cache *pMP3 = NULL; |
|
CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx ); |
|
if ( info ) |
|
{ |
|
pMP3 = new CAudioSourceMP3Cache( pSfx, info ); |
|
} |
|
else |
|
{ |
|
pMP3 = new CAudioSourceMP3Cache( pSfx ); |
|
} |
|
return pMP3; |
|
} |
|
|
|
#endif |
|
|
|
|