summaryrefslogtreecommitdiff
path: root/engine/audio/private/snd_wave_mixer_xma.cpp
diff options
context:
space:
mode:
authorFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
committerFluorescentCIAAfricanAmerican <[email protected]>2020-04-22 12:56:21 -0400
commit3bf9df6b2785fa6d951086978a3e66f49427166a (patch)
tree2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /engine/audio/private/snd_wave_mixer_xma.cpp
downloadarchived-source-engine-2018-hl2-src-master.tar.xz
archived-source-engine-2018-hl2-src-master.zip
Diffstat (limited to 'engine/audio/private/snd_wave_mixer_xma.cpp')
-rw-r--r--engine/audio/private/snd_wave_mixer_xma.cpp959
1 files changed, 959 insertions, 0 deletions
diff --git a/engine/audio/private/snd_wave_mixer_xma.cpp b/engine/audio/private/snd_wave_mixer_xma.cpp
new file mode 100644
index 0000000..13f183c
--- /dev/null
+++ b/engine/audio/private/snd_wave_mixer_xma.cpp
@@ -0,0 +1,959 @@
+//========= Copyright Valve Corporation, All rights reserved. ============//
+//
+// Purpose: XMA Decoding
+//
+//=====================================================================================//
+
+#include "audio_pch.h"
+#include "tier1/mempool.h"
+#include "circularbuffer.h"
+#include "tier1/utllinkedlist.h"
+
+// memdbgon must be the last include file in a .cpp file!!!
+#include "tier0/memdbgon.h"
+
+//#define DEBUG_XMA
+
+// Failed attempt to allow mixer to request data that is immediately discarded
+// to support < 0 delay samples
+//#define ALLOW_SKIP_SAMPLES
+
+// XMA is supposed to decode at an ideal max of 512 mono samples every 4msec.
+// XMA can only peel a max of 1984 stereo samples per poll request (if available).
+// Max is not achievable and degrades based on quality settings, stereo, etc, but using these numbers for for calcs.
+// 1984 stereo samples should be decoded by xma in 31 msec.
+// 1984 stereo samples at 44.1Khz dictates a request every 45 msec.
+// GetOutputData() must be clocked faster than 45 msec or samples will not be available.
+// However, the XMA decoder must be serviced much faster. It was designed for 5 msec.
+// 15 msec seems to be fast enough for XMA to decode enough to keep the smaller buffer sizes satisfied, and have slop for +/- 5 msec swings.
+
+// Need at least this amount of decoded pcm samples before mixing can commence.
+// This needs to be able to cover the initial mix request, while a new decode cycle is in flight.
+#define MIN_READYTOMIX ( ( 2 * XMA_POLL_RATE ) * 0.001f )
+
+// number of samples that xma decodes
+// must be 128 aligned for mono (1984 is hw max for stereo)
+#define XMA_MONO_OUTPUT_BUFFER_SAMPLES 2048
+#define XMA_STEREO_OUTPUT_BUFFER_SAMPLES 1920
+
+// for decoder input
+// xma blocks are fetched from the datacache into one of these hw buffers for decoding
+// must be in quantum units of XMA_BLOCK_SIZE
+#define XMA_INPUT_BUFFER_SIZE ( 8 * XMA_BLOCK_SIZE )
+
+// circular staging buffer to drain xma decoder and stage until mixer requests
+// must be large enough to hold the slowest expected mixing frame worth of samples
+#define PCM_STAGING_BUFFER_TIME 200
+
+// xma physical heap, supplies xma input buffers for hw decoder
+// each potential channel must be able to peel 2 buffers for driving xma decoder
+#define XMA_PHYSICAL_HEAP_SIZE ( 2 * MAX_CHANNELS * XMA_INPUT_BUFFER_SIZE )
+
+// in millseconds
+#define MIX_IO_DATA_TIMEOUT 2000 // async i/o from dvd could be very late
+#define MIX_DECODER_TIMEOUT 3000 // decoder might be very busy
+#define MIX_DECODER_POLLING_LATENCY 5 // not faster than 5ms, or decoder will sputter
+
+// diagnostic errors
+#define ERROR_IO_DATA_TIMEOUT -1 // async i/o taking too long to deliver xma blocks
+#define ERROR_IO_TRUNCATED_BLOCK -2 // async i/o failed to deliver complete blocks
+#define ERROR_IO_NO_XMA_DATA -3 // async i/o failed to deliver any block
+#define ERROR_DECODER_TIMEOUT -4 // decoder taking too long to decode xma blocks
+#define ERROR_OUT_OF_MEMORY -5 // not enough physical memory for xma blocks
+#define ERROR_XMA_PARSE -6 // decoder barfed on xma blocks
+#define ERROR_XMA_CANTLOCK -7 // hw not acting as expected
+#define ERROR_XMA_CANTSUBMIT -8 // hw not acting as expected
+#define ERROR_XMA_CANTRESUME -9 // hw not acting as expected
+#define ERROR_XMA_NO_PCM_DATA -10 // no xma decoded pcm data ready
+#define ERROR_NULL_BUFFER -11 // logic flaw, expected buffer is null
+
+const char *g_XMAErrorStrings[] =
+{
+ "Unknown Error Code",
+ "Async I/O Data Timeout", // ERROR_IO_DATA_TIMEOUT
+ "Async I/O Truncated Block", // ERROR_IO_TRUNCATED_BLOCK
+ "Async I/O Data Not Ready", // ERROR_IO_NO_XMA_DATA
+ "Decoder Timeout", // ERROR_DECODER_TIMEOUT
+ "Out Of Memory", // ERROR_OUT_OF_MEMORY
+ "XMA Parse", // ERROR_XMA_PARSE
+ "XMA Cannot Lock", // ERROR_XMA_CANTLOCK
+ "XMA Cannot Submit", // ERROR_XMA_CANTSUBMIT
+ "XMA Cannot Resume", // ERROR_XMA_CANTRESUME
+ "XMA No PCM Data Ready", // ERROR_XMA_NO_PCM_DATA
+ "NULL Buffer", // ERROR_NULL_BUFFER
+};
+
+class CXMAAllocator
+{
+public:
+ static void *Alloc( int bytes )
+ {
+ MEM_ALLOC_CREDIT();
+
+ return XMemAlloc( bytes,
+ MAKE_XALLOC_ATTRIBUTES(
+ 0,
+ false,
+ TRUE,
+ FALSE,
+ eXALLOCAllocatorId_XAUDIO,
+ XALLOC_PHYSICAL_ALIGNMENT_4K,
+ XALLOC_MEMPROTECT_WRITECOMBINE_LARGE_PAGES,
+ FALSE,
+ XALLOC_MEMTYPE_PHYSICAL ) );
+ }
+
+ static void Free( void *p )
+ {
+ XMemFree( p,
+ MAKE_XALLOC_ATTRIBUTES(
+ 0,
+ false,
+ TRUE,
+ FALSE,
+ eXALLOCAllocatorId_XAUDIO,
+ XALLOC_PHYSICAL_ALIGNMENT_4K,
+ XALLOC_MEMPROTECT_WRITECOMBINE_LARGE_PAGES,
+ FALSE,
+ XALLOC_MEMTYPE_PHYSICAL ) );
+ }
+};
+
+// for XMA decoding, fixed size allocations aligned to 4K from a single physical heap
+CAlignedMemPool< XMA_INPUT_BUFFER_SIZE, 4096, XMA_PHYSICAL_HEAP_SIZE, CXMAAllocator > g_XMAMemoryPool;
+
+ConVar snd_xma_spew_warnings( "snd_xma_spew_warnings", "0" );
+ConVar snd_xma_spew_startup( "snd_xma_spew_startup", "0" );
+ConVar snd_xma_spew_mixers( "snd_xma_spew_mixers", "0" );
+ConVar snd_xma_spew_decode( "snd_xma_spew_decode", "0" );
+ConVar snd_xma_spew_drain( "snd_xma_spew_drain", "0" );
+#ifdef DEBUG_XMA
+ConVar snd_xma_record( "snd_xma_record", "0" );
+ConVar snd_xma_spew_errors( "snd_xma_spew_errors", "0" );
+#endif
+
+//-----------------------------------------------------------------------------
+// Purpose: Mixer for ADPCM encoded audio
+//-----------------------------------------------------------------------------
+class CAudioMixerWaveXMA : public CAudioMixerWave
+{
+public:
+ typedef CAudioMixerWave BaseClass;
+
+ CAudioMixerWaveXMA( IWaveData *data, int initialStreamPosition );
+ ~CAudioMixerWaveXMA( void );
+
+ virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress );
+
+ virtual int GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] );
+
+ virtual void SetSampleStart( int newPosition );
+ virtual int GetPositionForSave();
+ virtual void SetPositionFromSaved( int savedPosition );
+
+ virtual int GetMixSampleSize() { return CalcSampleSize( 16, m_NumChannels ); }
+
+ virtual bool IsReadyToMix();
+ virtual bool ShouldContinueMixing();
+
+private:
+ int GetXMABlocksAndSubmitToDecoder( bool bDecoderLocked );
+ int UpdatePositionForLooping( int *pNumRequestedSamples );
+ int ServiceXMADecoder( bool bForceUpdate );
+ int GetPCMSamples( int numRequested, char *pData );
+
+ XMAPLAYBACK *m_pXMAPlayback;
+
+ // input buffers, encoded xma
+ byte *m_pXMABuffers[2];
+ int m_XMABufferIndex;
+
+ // output buffer, decoded pcm samples, a staging circular buffer, waiting for mixer requests
+ // due to staging nature, contains decoded samples from multiple input buffers
+ CCircularBuffer *m_pPCMSamples;
+
+ int m_SampleRate;
+ int m_NumChannels;
+ // maximum possible decoded samples
+ int m_SampleCount;
+
+ // decoded sample position
+ int m_SamplePosition;
+ // current data marker
+ int m_LastDataOffset;
+ int m_DataOffset;
+ // total bytes of data
+ int m_TotalBytes;
+
+#if defined( ALLOW_SKIP_SAMPLES )
+ // number of samples to throwaway
+ int m_SkipSamples;
+#endif
+
+ // timers
+ unsigned int m_StartTime;
+ unsigned int m_LastDrainTime;
+ unsigned int m_LastPollTime;
+
+ int m_hMixerList;
+ int m_Error;
+
+ unsigned int m_bStartedMixing : 1;
+ unsigned int m_bFinished : 1;
+ unsigned int m_bLooped : 1;
+};
+
+CUtlFixedLinkedList< CAudioMixerWaveXMA * > g_XMAMixerList;
+
+CON_COMMAND( snd_xma_info, "Spew XMA Info" )
+{
+ Msg( "XMA Memory:\n" );
+ Msg( " Blocks Allocated: %d\n", g_XMAMemoryPool.NumAllocated() );
+ Msg( " Blocks Free: %d\n", g_XMAMemoryPool.NumFree() );
+ Msg( " Total Bytes: %d\n", g_XMAMemoryPool.BytesTotal() );
+ Msg( "Active XMA Mixers: %d\n", g_XMAMixerList.Count() );
+ for ( int hMixer = g_XMAMixerList.Head(); hMixer != g_XMAMixerList.InvalidIndex(); hMixer = g_XMAMixerList.Next( hMixer ) )
+ {
+ CAudioMixerWaveXMA *pXMAMixer = g_XMAMixerList[hMixer];
+ Msg( " rate:%5d ch:%1d '%s'\n", pXMAMixer->GetSource()->SampleRate(), pXMAMixer->GetSource()->IsStereoWav() ? 2 : 1, pXMAMixer->GetSource()->GetFileName() );
+ }
+}
+
+CAudioMixerWaveXMA::CAudioMixerWaveXMA( IWaveData *data, int initialStreamPosition ) : CAudioMixerWave( data )
+{
+ Assert( dynamic_cast<CAudioSourceWave *>(&m_pData->Source()) != NULL );
+
+ m_Error = 0;
+
+ m_NumChannels = m_pData->Source().IsStereoWav() ? 2 : 1;
+ m_SampleRate = m_pData->Source().SampleRate();
+ m_bLooped = m_pData->Source().IsLooped();
+ m_SampleCount = m_pData->Source().SampleCount();
+ m_TotalBytes = m_pData->Source().DataSize();
+
+#if defined( ALLOW_SKIP_SAMPLES )
+ m_SkipSamples = 0;
+#endif
+
+ m_LastDataOffset = initialStreamPosition;
+ m_DataOffset = initialStreamPosition;
+ m_SamplePosition = 0;
+ if ( initialStreamPosition )
+ {
+ m_SamplePosition = m_pData->Source().StreamToSamplePosition( initialStreamPosition );
+
+ CAudioMixerWave::m_sample_loaded_index = m_SamplePosition;
+ CAudioMixerWave::m_sample_max_loaded = m_SamplePosition + 1;
+ }
+
+ m_bStartedMixing = false;
+ m_bFinished = false;
+
+ m_StartTime = 0;
+ m_LastPollTime = 0;
+ m_LastDrainTime = 0;
+
+ m_pXMAPlayback = NULL;
+ m_pPCMSamples = NULL;
+
+ m_pXMABuffers[0] = NULL;
+ m_pXMABuffers[1] = NULL;
+ m_XMABufferIndex = 0;
+
+ m_hMixerList = g_XMAMixerList.AddToTail( this );
+
+#ifdef DEBUG_XMA
+ if ( snd_xma_record.GetBool() )
+ {
+ WaveCreateTmpFile( "debug.wav", m_SampleRate, 16, m_NumChannels );
+ }
+#endif
+
+ if ( snd_xma_spew_mixers.GetBool() )
+ {
+ Msg( "XMA: 0x%8.8x (%2d), Mixer Alloc, '%s'\n", (unsigned int)this, g_XMAMixerList.Count(), m_pData->Source().GetFileName() );
+ }
+}
+
+CAudioMixerWaveXMA::~CAudioMixerWaveXMA( void )
+{
+ if ( m_pXMAPlayback )
+ {
+ XMAPlaybackDestroy( m_pXMAPlayback );
+
+ g_XMAMemoryPool.Free( m_pXMABuffers[0] );
+ if ( m_pXMABuffers[1] )
+ {
+ g_XMAMemoryPool.Free( m_pXMABuffers[1] );
+ }
+ }
+
+ if ( m_pPCMSamples )
+ {
+ FreeCircularBuffer( m_pPCMSamples );
+ }
+
+ g_XMAMixerList.Remove( m_hMixerList );
+
+ if ( snd_xma_spew_mixers.GetBool() )
+ {
+ Msg( "XMA: 0x%8.8x (%2d), Mixer Freed, '%s'\n", (unsigned int)this, g_XMAMixerList.Count(), m_pData->Source().GetFileName() );
+ }
+}
+
+void CAudioMixerWaveXMA::Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress )
+{
+ if ( m_NumChannels == 1 )
+ {
+ pDevice->Mix16Mono( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress );
+ }
+ else
+ {
+ pDevice->Mix16Stereo( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress );
+ }
+}
+
+//-----------------------------------------------------------------------------
+// Looping is achieved in two passes to provide a circular view of the linear data.
+// Pass1: Clamps a sample request to the end of data.
+// Pass2: Snaps to the loop start, and returns the number of samples to discard, could be 0,
+// up to the expected loop sample position.
+// Returns the number of samples to discard, or 0.
+//-----------------------------------------------------------------------------
+int CAudioMixerWaveXMA::UpdatePositionForLooping( int *pNumRequestedSamples )
+{
+ if ( !m_bLooped )
+ {
+ // not looping, no fixups
+ return 0;
+ }
+
+ int numLeadingSamples;
+ int numTrailingSamples;
+ CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source());
+ int loopSampleStart = source.GetLoopingInfo( NULL, &numLeadingSamples, &numTrailingSamples );
+
+ int numRemainingSamples = ( m_SampleCount - numTrailingSamples ) - m_SamplePosition;
+
+ // possibly straddling the end of data (and thus about to loop)
+ // want to split the straddle into two regions, due to loops possibly requiring a trailer and leader of discarded samples
+ if ( numRemainingSamples > 0 )
+ {
+ // first region, all the remaining samples, clamped until end of desired data
+ *pNumRequestedSamples = min( *pNumRequestedSamples, numRemainingSamples );
+
+ // nothing to discard
+ return 0;
+ }
+ else if ( numRemainingSamples == 0 )
+ {
+ // at exact end of desired data, snap the sample position back
+ // the position will be correct AFTER discarding decoded trailing and leading samples
+ m_SamplePosition = loopSampleStart;
+
+ // clamp the request
+ numRemainingSamples = ( m_SampleCount - numTrailingSamples ) - m_SamplePosition;
+ *pNumRequestedSamples = min( *pNumRequestedSamples, numRemainingSamples );
+
+ // flush these samples so the sample position is the real loop sample starting position
+ return numTrailingSamples + numLeadingSamples;
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Get and submit XMA block(s). The decoder must stay blocks ahead of mixer
+// so the decoded samples are available for peeling.
+// An XMA file is thus treated as a series of fixed size large buffers (multiple xma blocks),
+// which are streamed in sequentially. The XMA buffers may be delayed from the
+// audio data cache due to async i/o latency.
+// Returns < 0 if error, 0 if no decode started, 1 if decode submitted.
+//-----------------------------------------------------------------------------
+int CAudioMixerWaveXMA::GetXMABlocksAndSubmitToDecoder( bool bDecoderIsLocked )
+{
+ int status = 0;
+
+ if ( m_DataOffset >= m_TotalBytes )
+ {
+ if ( !m_bLooped )
+ {
+ // end of file, no more data to decode
+ // not an error, because decoder finishes long before samples drained
+ return 0;
+ }
+
+ // start from beginning of loop
+ CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source());
+ source.GetLoopingInfo( &m_DataOffset, NULL, NULL );
+ m_DataOffset *= XMA_BLOCK_SIZE;
+ }
+
+ HRESULT hr;
+ bool bLocked = false;
+ if ( !bDecoderIsLocked )
+ {
+ // decoder must be locked before any access
+ hr = XMAPlaybackRequestModifyLock( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ status = ERROR_XMA_CANTLOCK;
+ goto cleanUp;
+ }
+
+ hr = XMAPlaybackWaitUntilModifyLockObtained( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ status = ERROR_XMA_CANTLOCK;
+ goto cleanUp;
+ }
+ bLocked = true;
+ }
+
+ // the input buffer can never be less than a single xma block (buffer size is multiple blocks)
+ int bufferSize = min( m_TotalBytes - m_DataOffset, XMA_INPUT_BUFFER_SIZE );
+ if ( !bufferSize )
+ {
+ // EOF
+ goto cleanUp;
+ }
+ Assert( !( bufferSize % XMA_BLOCK_SIZE ) );
+
+ byte *pXMABuffer = m_pXMABuffers[m_XMABufferIndex & 0x01];
+ if ( !pXMABuffer )
+ {
+ // shouldn't happen, buffer should have been allocated
+ Assert( 0 );
+ status = ERROR_NULL_BUFFER;
+ goto cleanUp;
+ }
+
+ if ( !XMAPlaybackQueryReadyForMoreData( m_pXMAPlayback, 0 ) || XMAPlaybackQueryInputDataPending( m_pXMAPlayback, 0, pXMABuffer ) )
+ {
+ // decoder too saturated for more data or
+ // decoder still decoding from input hw buffer
+ goto cleanUp;
+ }
+
+ // get xma block(s)
+ // pump to get all of requested data
+ char *pData;
+ int total = 0;
+ while ( total < bufferSize )
+ {
+ int available = m_pData->ReadSourceData( (void **)&pData, m_DataOffset, bufferSize - total, NULL );
+ if ( !available )
+ break;
+
+ // aggregate into hw buffer
+ V_memcpy( pXMABuffer + total, pData, available );
+
+ m_DataOffset += available;
+ total += available;
+ }
+ if ( total != bufferSize )
+ {
+ if ( !total )
+ {
+ // failed to get any data, could be async latency or file error
+ status = ERROR_IO_NO_XMA_DATA;
+ }
+ else
+ {
+ // failed to get complete xma block(s)
+ status = ERROR_IO_TRUNCATED_BLOCK;
+ }
+ goto cleanUp;
+ }
+
+ // track the currently submitted offset
+ // this is used as a cheap method for save/restore because an XMA seek table is not available
+ m_LastDataOffset = m_DataOffset - total;
+
+ // start decoding the block(s) in the hw buffer
+ hr = XMAPlaybackSubmitData( m_pXMAPlayback, 0, pXMABuffer, bufferSize );
+ if ( FAILED( hr ) )
+ {
+ // failed to start decoder
+ status = ERROR_XMA_CANTSUBMIT;
+ goto cleanUp;
+ }
+
+ // decode submitted
+ status = 1;
+
+ // advance to next buffer
+ m_XMABufferIndex++;
+
+ if ( snd_xma_spew_decode.GetBool() )
+ {
+ Msg( "XMA: 0x%8.8x, XMABuffer: 0x%8.8x, BufferSize: %d, NextDataOffset: %d, %s\n", (unsigned int)this, pXMABuffer, bufferSize, m_DataOffset, m_pData->Source().GetFileName() );
+ }
+
+cleanUp:
+ if ( bLocked )
+ {
+ // release the lock and let the decoder run
+ hr = XMAPlaybackResumePlayback( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ status = ERROR_XMA_CANTRESUME;
+ }
+ }
+
+ return status;
+}
+
+//-----------------------------------------------------------------------------
+// Drain the XMA Decoder into the staging circular buffer of PCM for mixer.
+// Fetch new XMA samples for the decoder.
+//-----------------------------------------------------------------------------
+int CAudioMixerWaveXMA::ServiceXMADecoder( bool bForceUpdate )
+{
+ // allow decoder to work without being polled (lock causes a decoding stall)
+ // decoder must be allowed minimum operating latency
+ // the buffers are sized to compensate for the operating latency
+ if ( !bForceUpdate && ( Plat_MSTime() - m_LastPollTime <= MIX_DECODER_POLLING_LATENCY ) )
+ {
+ return 0;
+ }
+ m_LastPollTime = Plat_MSTime();
+
+ // lock and pause the decoder to gain access
+ HRESULT hr = XMAPlaybackRequestModifyLock( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ m_Error = ERROR_XMA_CANTLOCK;
+ return -1;
+ }
+
+ hr = XMAPlaybackWaitUntilModifyLockObtained( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ m_Error = ERROR_XMA_CANTLOCK;
+ return -1;
+ }
+
+ DWORD dwParseError = XMAPlaybackGetParseError( m_pXMAPlayback, 0 );
+ if ( dwParseError )
+ {
+ if ( snd_xma_spew_warnings.GetBool() )
+ {
+ Warning( "XMA: 0x%8.8x, Decoder Error, Parse: %d, '%s'\n", (unsigned int)this, dwParseError, m_pData->Source().GetFileName() );
+ }
+ m_Error = ERROR_XMA_PARSE;
+ return -1;
+ }
+
+#ifdef DEBUG_XMA
+ if ( snd_xma_spew_errors.GetBool() )
+ {
+ DWORD dwError = XMAPlaybackGetErrorBits( m_pXMAPlayback, 0 );
+ if ( dwError )
+ {
+ Warning( "XMA: 0x%8.8x, Playback Error: %d, '%s'\n", (unsigned int)this, dwError, m_pData->Source().GetFileName() );
+ }
+ }
+#endif
+
+ int numNewSamples = XMAPlaybackQueryAvailableData( m_pXMAPlayback, 0 );
+ int numMaxSamples = m_pPCMSamples->GetWriteAvailable()/( m_NumChannels*sizeof( short ) );
+ int numSamples = min( numNewSamples, numMaxSamples );
+ while ( numSamples )
+ {
+ char *pPCMData = NULL;
+ int numSamplesDecoded = XMAPlaybackConsumeDecodedData( m_pXMAPlayback, 0, numSamples, (void**)&pPCMData );
+
+ // put into staging buffer, ready for mixer to drain
+ m_pPCMSamples->Write( pPCMData, numSamplesDecoded*m_NumChannels*sizeof( short ) );
+
+ numSamples -= numSamplesDecoded;
+ numNewSamples -= numSamplesDecoded;
+ }
+
+ // queue up more blocks for the decoder
+ // the decoder will always finish ahead of the mixer, submit nothing, and the mixer will still be draining
+ int decodeStatus = GetXMABlocksAndSubmitToDecoder( true );
+ if ( decodeStatus < 0 )
+ {
+ m_Error = decodeStatus;
+ return -1;
+ }
+
+ m_bFinished = ( numNewSamples == 0 ) && ( decodeStatus == 0 ) && XMAPlaybackIsIdle( m_pXMAPlayback, 0 );
+
+ // decoder was paused for access, let the decoder run
+ hr = XMAPlaybackResumePlayback( m_pXMAPlayback );
+ if ( FAILED( hr ) )
+ {
+ m_Error = ERROR_XMA_CANTRESUME;
+ return -1;
+ }
+
+ return 1;
+}
+
+//-----------------------------------------------------------------------------
+// Drain the PCM staging buffer.
+// Copy samples (numSamplesToCopy && pData). Return actual copied.
+// Flush Samples (numSamplesToCopy && !pData). Return actual flushed.
+// Query available number of samples (!numSamplesToCopy && !pData). Returns available.
+//-----------------------------------------------------------------------------
+int CAudioMixerWaveXMA::GetPCMSamples( int numSamplesToCopy, char *pData )
+{
+ int numReadySamples = m_pPCMSamples->GetReadAvailable()/( m_NumChannels*sizeof( short ) );
+
+ // peel sequential samples from the stream's staging buffer
+ int numCopiedSamples = 0;
+ int numRequestedSamples = min( numSamplesToCopy, numReadySamples );
+ if ( numRequestedSamples )
+ {
+ if ( pData )
+ {
+ // copy to caller
+ m_pPCMSamples->Read( pData, numRequestedSamples*m_NumChannels*sizeof( short ) );
+ pData += numRequestedSamples*m_NumChannels*sizeof( short );
+ }
+ else
+ {
+ // flush
+ m_pPCMSamples->Advance( numRequestedSamples*m_NumChannels*sizeof( short ) );
+ }
+
+ numCopiedSamples += numRequestedSamples;
+ }
+
+ if ( snd_xma_spew_drain.GetBool() )
+ {
+ char *pOperation = ( numSamplesToCopy && !pData ) ? "Flushed" : "Copied";
+ Msg( "XMA: 0x%8.8x, SamplePosition: %d, Ready: %d, Requested: %d, %s: %d, Elapsed: %d ms '%s'\n",
+ (unsigned int)this, m_SamplePosition, numReadySamples, numSamplesToCopy, pOperation, numCopiedSamples, Plat_MSTime() - m_LastDrainTime, m_pData->Source().GetFileName() );
+ }
+ m_LastDrainTime = Plat_MSTime();
+
+ if ( numSamplesToCopy )
+ {
+ // could be actual flushed or actual copied
+ return numCopiedSamples;
+ }
+
+ if ( !pData )
+ {
+ // satify query for available
+ return numReadySamples;
+ }
+
+ return 0;
+}
+
+//-----------------------------------------------------------------------------
+// Stall mixing until initial buffer of decoded samples are available.
+//-----------------------------------------------------------------------------
+bool CAudioMixerWaveXMA::IsReadyToMix()
+{
+ // XMA mixing cannot be driven from the main thread
+ Assert( ThreadInMainThread() == false );
+
+ if ( m_Error )
+ {
+ // error has been set
+ // let mixer try to get unavailable samples, which casues the real abort
+ return true;
+ }
+
+ if ( m_bStartedMixing )
+ {
+ // decoding process has started
+ return true;
+ }
+
+ if ( !m_pXMAPlayback )
+ {
+ // first time, finish setup
+ int numBuffers;
+ if ( m_bLooped || m_TotalBytes > XMA_INPUT_BUFFER_SIZE )
+ {
+ // data will cascade through multiple buffers
+ numBuffers = 2;
+ }
+ else
+ {
+ // data can fit into a single buffer
+ numBuffers = 1;
+ }
+
+ // xma data must be decoded from a hw friendly buffer
+ // pool should have buffers available
+ if ( g_XMAMemoryPool.BytesAllocated() != numBuffers * g_XMAMemoryPool.ChunkSize() )
+ {
+ for ( int i = 0; i < numBuffers; i++ )
+ {
+ m_pXMABuffers[i] = (byte*)g_XMAMemoryPool.Alloc();
+ }
+
+ XMA_PLAYBACK_INIT xmaPlaybackInit = { 0 };
+ xmaPlaybackInit.sampleRate = m_SampleRate;
+ xmaPlaybackInit.channelCount = m_NumChannels;
+ xmaPlaybackInit.subframesToDecode = 4;
+ xmaPlaybackInit.outputBufferSizeInSamples = ( m_NumChannels == 2 ) ? XMA_STEREO_OUTPUT_BUFFER_SAMPLES : XMA_MONO_OUTPUT_BUFFER_SAMPLES;
+ XMAPlaybackCreate( 1, &xmaPlaybackInit, 0, &m_pXMAPlayback );
+
+ int stagingSize = PCM_STAGING_BUFFER_TIME * m_SampleRate * m_NumChannels * sizeof( short ) * 0.001f;
+ m_pPCMSamples = AllocateCircularBuffer( AlignValue( stagingSize, 4 ) );
+ }
+ else
+ {
+ // too many sounds playing, no xma buffers free
+ m_Error = ERROR_OUT_OF_MEMORY;
+ return true;
+ }
+
+ m_StartTime = Plat_MSTime();
+ }
+
+ // waiting for samples
+ // allow decoder to work without being polled (lock causes a decoding stall)
+ if ( Plat_MSTime() - m_LastPollTime <= MIX_DECODER_POLLING_LATENCY )
+ {
+ return false;
+ }
+ m_LastPollTime = Plat_MSTime();
+
+ // must have buffers in flight before mixing can begin
+ if ( m_DataOffset == m_LastDataOffset )
+ {
+ // keep trying to get data, async i/o has some allowable latency
+ int decodeStatus = GetXMABlocksAndSubmitToDecoder( false );
+ if ( decodeStatus < 0 && decodeStatus != ERROR_IO_NO_XMA_DATA )
+ {
+ m_Error = decodeStatus;
+ return true;
+ }
+ else if ( !decodeStatus || decodeStatus == ERROR_IO_NO_XMA_DATA )
+ {
+ // async streaming latency could be to blame, check watchdog
+ if ( Plat_MSTime() - m_StartTime >= MIX_IO_DATA_TIMEOUT )
+ {
+ m_Error = ERROR_IO_DATA_TIMEOUT;
+ }
+ return false;
+ }
+ }
+
+ // get the available samples ready for immediate mixing
+ if ( ServiceXMADecoder( true ) < 0 )
+ {
+ return true;
+ }
+
+ // can't mix until we have a minimum threshold of data or the decoder is finished
+ int minSamplesNeeded = m_bFinished ? 0 : MIN_READYTOMIX * m_SampleRate;
+#if defined( ALLOW_SKIP_SAMPLES )
+ minSamplesNeeded += m_bFinished ? 0 : m_SkipSamples;
+#endif
+
+ int numReadySamples = GetPCMSamples( 0, NULL );
+ if ( numReadySamples > minSamplesNeeded )
+ {
+ // decoder has samples ready for draining
+ m_bStartedMixing = true;
+ if ( snd_xma_spew_startup.GetBool() )
+ {
+ Msg( "XMA: 0x%8.8x, Startup Latency: %d ms, Samples Ready: %d, '%s'\n", (unsigned int)this, Plat_MSTime() - m_StartTime, numReadySamples, m_pData->Source().GetFileName() );
+ }
+ return true;
+ }
+
+ if ( Plat_MSTime() - m_StartTime >= MIX_DECODER_TIMEOUT )
+ {
+ m_Error = ERROR_DECODER_TIMEOUT;
+ }
+
+ // on startup error, let mixer start and get unavailable samples, and abort
+ // otherwise hold off mixing until samples arrive
+ return ( m_Error != 0 );
+}
+
+//-----------------------------------------------------------------------------
+// Returns true to mix, false to stop mixer completely. Called after
+// mixer requests samples.
+//-----------------------------------------------------------------------------
+bool CAudioMixerWaveXMA::ShouldContinueMixing()
+{
+ if ( !IsRetail() && m_Error && snd_xma_spew_warnings.GetBool() )
+ {
+ const char *pErrorString;
+ if ( m_Error < 0 && -m_Error < ARRAYSIZE( g_XMAErrorStrings ) )
+ {
+ pErrorString = g_XMAErrorStrings[-m_Error];
+ }
+ else
+ {
+ pErrorString = g_XMAErrorStrings[0];
+ }
+ Warning( "XMA: 0x%8.8x, Mixer Aborted: %s, SamplePosition: %d/%d, DataOffset: %d/%d, '%s'\n",
+ (unsigned int)this, pErrorString, m_SamplePosition, m_SampleCount, m_DataOffset, m_TotalBytes, m_pData->Source().GetFileName() );
+ }
+
+ // an error condition is fatal to mixer
+ return ( m_Error == 0 && BaseClass::ShouldContinueMixing() );
+}
+
+//-----------------------------------------------------------------------------
+// Read existing buffer or decompress a new block when necessary.
+// If no samples can be fetched, returns 0, which hints the mixer to a pending shutdown state.
+// This routines operates in large buffer quantums, and nothing smaller.
+// XMA decode performance severly degrades if the lock is too frequent.
+//-----------------------------------------------------------------------------
+int CAudioMixerWaveXMA::GetOutputData( void **pData, int numSamplesToCopy, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] )
+{
+ if ( m_Error )
+ {
+ // mixer will eventually shutdown
+ return 0;
+ }
+
+ if ( !m_bStartedMixing )
+ {
+#if defined( ALLOW_SKIP_SAMPLES )
+ int numMaxSamples = AUDIOSOURCE_COPYBUF_SIZE/( m_NumChannels * sizeof( short ) );
+ numSamplesToCopy = min( numSamplesToCopy, numMaxSamples );
+ m_SkipSamples += numSamplesToCopy;
+
+ // caller requesting data before mixing has commenced
+ V_memset( copyBuf, 0, numSamplesToCopy );
+ *pData = (void*)copyBuf;
+ return numSamplesToCopy;
+#else
+ // not allowed, GetOutputData() should only be called by the mixing loop
+ Assert( 0 );
+ return 0;
+#endif
+ }
+
+ // XMA mixing cannot be driven from the main thread
+ Assert( ThreadInMainThread() == false );
+
+ // needs to be clocked at regular intervals
+ if ( ServiceXMADecoder( false ) < 0 )
+ {
+ return 0;
+ }
+
+#if defined( ALLOW_SKIP_SAMPLES )
+ if ( m_SkipSamples > 0 )
+ {
+ // flush whatever is available
+ // ignore
+ m_SkipSamples -= GetPCMSamples( m_SkipSamples, NULL );
+ if ( m_SkipSamples != 0 )
+ {
+ // not enough decoded data ready to flush
+ // must flush these samples to maintain proper position
+ m_Error = ERROR_XMA_NO_PCM_DATA;
+ return 0;
+ }
+ }
+#endif
+
+ // loopback may require flushing some decoded samples
+ int numRequestedSamples = numSamplesToCopy;
+ int numDiscardSamples = UpdatePositionForLooping( &numRequestedSamples );
+ if ( numDiscardSamples > 0 )
+ {
+ // loopback requires discarding samples to converge to expected looppoint
+ numDiscardSamples -= GetPCMSamples( numDiscardSamples, NULL );
+ if ( numDiscardSamples != 0 )
+ {
+ // not enough decoded data ready to flush
+ // must flush these samples to achieve looping
+ m_Error = ERROR_XMA_NO_PCM_DATA;
+ return 0;
+ }
+ }
+
+ // can only drain as much as can be copied to caller
+ int numMaxSamples = AUDIOSOURCE_COPYBUF_SIZE/( m_NumChannels * sizeof( short ) );
+ numRequestedSamples = min( numRequestedSamples, numMaxSamples );
+
+ int numCopiedSamples = GetPCMSamples( numRequestedSamples, copyBuf );
+ if ( numCopiedSamples )
+ {
+ CAudioMixerWave::m_sample_max_loaded += numCopiedSamples;
+ CAudioMixerWave::m_sample_loaded_index += numCopiedSamples;
+
+ // advance position by valid samples
+ m_SamplePosition += numCopiedSamples;
+
+ *pData = (void*)copyBuf;
+
+#ifdef DEBUG_XMA
+ if ( snd_xma_record.GetBool() )
+ {
+ WaveAppendTmpFile( "debug.wav", copyBuf, 16, numCopiedSamples * m_NumChannels );
+ WaveFixupTmpFile( "debug.wav" );
+ }
+#endif
+ }
+ else
+ {
+ // no samples copied
+ if ( !m_bFinished && numRequestedSamples )
+ {
+ // XMA latency error occurs when decoder not finished (not at EOF) and caller wanted samples but can't get any
+ if ( snd_xma_spew_warnings.GetInt() )
+ {
+ Warning( "XMA: 0x%8.8x, No Decoded Data Ready: %d samples needed, '%s'\n", (unsigned int)this, numSamplesToCopy, m_pData->Source().GetFileName() );
+ }
+ m_Error = ERROR_XMA_NO_PCM_DATA;
+ }
+ }
+
+ return numCopiedSamples;
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Seek to a new position in the file
+// NOTE: In most cases, only call this once, and call it before playing
+// any data.
+// Input : newPosition - new position in the sample clocks of this sample
+//-----------------------------------------------------------------------------
+void CAudioMixerWaveXMA::SetSampleStart( int newPosition )
+{
+ // cannot support this
+ // this should be unused and thus not supporting
+ Assert( 0 );
+}
+
+
+int CAudioMixerWaveXMA::GetPositionForSave()
+{
+ if ( m_bLooped )
+ {
+ // A looped sample cannot be saved/restored because the decoded sample position,
+ // which is needed for loop calc, cannot ever be correctly restored without
+ // the XMA seek table.
+ return 0;
+ }
+
+ // This is silly and totally wrong, but doing it anyways.
+ // The correct thing was to have the XMA seek table and use
+ // that to determine the correct packet. This is just a hopeful
+ // nearby approximation. Music did not have the seek table at
+ // the time of this code. The Seek table was added for vo
+ // restoration later.
+ return m_LastDataOffset;
+}
+
+void CAudioMixerWaveXMA::SetPositionFromSaved( int savedPosition )
+{
+ // Not used here. The Mixer creation will be given the initial startup offset.
+}
+
+//-----------------------------------------------------------------------------
+// Purpose: Abstract factory function for XMA mixers
+//-----------------------------------------------------------------------------
+CAudioMixer *CreateXMAMixer( IWaveData *data, int initialStreamPosition )
+{
+ return new CAudioMixerWaveXMA( data, initialStreamPosition );
+}