summaryrefslogtreecommitdiff
path: root/engine/audio/private/snd_mp3_source.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'engine/audio/private/snd_mp3_source.cpp')
-rw-r--r--engine/audio/private/snd_mp3_source.cpp602
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
+