diff options
Diffstat (limited to 'engine/audio/private/snd_mp3_source.cpp')
| -rw-r--r-- | engine/audio/private/snd_mp3_source.cpp | 602 |
1 files changed, 602 insertions, 0 deletions
diff --git a/engine/audio/private/snd_mp3_source.cpp b/engine/audio/private/snd_mp3_source.cpp new file mode 100644 index 0000000..d4b012e --- /dev/null +++ b/engine/audio/private/snd_mp3_source.cpp @@ -0,0 +1,602 @@ +//========= 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; + + int 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 ); + + int 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 + |