diff options
Diffstat (limited to 'engine/audio/private/snd_wave_mixer_xma.cpp')
| -rw-r--r-- | engine/audio/private/snd_wave_mixer_xma.cpp | 959 |
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 ); +} |