|
|
//========= 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
|