//======================================================================= // Copyright XashXT Group 2009 © // s_direct.c - sound hardware output //======================================================================= #include #include "sound.h" #define iDirectSoundCreate( a, b, c ) pDirectSoundCreate( a, b, c ) static HRESULT ( _stdcall *pDirectSoundCreate)(GUID* lpGUID, LPDIRECTSOUND* lplpDS, IUnknown* pUnkOuter ); static dllfunc_t dsound_funcs[] = { { "DirectSoundCreate", (void **) &pDirectSoundCreate }, { NULL, NULL } }; dll_info_t dsound_dll = { "dsound.dll", dsound_funcs, NULL, NULL, NULL, false, 0, 0 }; // 64K is > 1 second at 16-bit, 22050 Hz #define WAV_BUFFERS 64 #define WAV_MASK 0x3F #define WAV_BUFFER_SIZE 0x0400 #define SECONDARY_BUFFER_SIZE 0x10000 typedef enum { SIS_SUCCESS, SIS_FAILURE, SIS_NOTAVAIL } si_state_t; cvar_t *s_wavonly; static HWND snd_hwnd; static bool dsound_init; static bool wav_init; static bool snd_firsttime = true; static bool snd_isdirect, snd_iswave; static bool primary_format_set; static int snd_buffer_count = 0; static int snd_sent, snd_completed; static int sample16; /* ======================================================================= Global variables. Must be visible to window-procedure function so it can unlock and free the data block after it has been played. ======================================================================= */ DWORD locksize; HANDLE hData; HPSTR lpData, lpData2; HGLOBAL hWaveHdr; LPWAVEHDR lpWaveHdr; HWAVEOUT hWaveOut; WAVEOUTCAPS wavecaps; DWORD gSndBufSize; MMTIME mmstarttime; LPDIRECTSOUNDBUFFER pDSBuf, pDSPBuf; LPDIRECTSOUND pDS; bool SNDDMA_InitDirect( void *hInst ); bool SNDDMA_InitWav( void ); void SNDDMA_FreeSound( void ); static const char *DSoundError( int error ) { switch( error ) { case DSERR_BUFFERLOST: return "DSERR_BUFFERLOST"; case DSERR_INVALIDCALL: return "DSERR_INVALIDCALLS"; case DSERR_INVALIDPARAM: return "DSERR_INVALIDPARAM"; case DSERR_PRIOLEVELNEEDED: return "DSERR_PRIOLEVELNEEDED"; } return "Unknown Error"; } /* ================== DS_CreateBuffers ================== */ static bool DS_CreateBuffers( void *hInst ) { DSBUFFERDESC dsbuf; DSBCAPS dsbcaps; WAVEFORMATEX pformat, format; Mem_Set( &format, 0, sizeof( format )); format.wFormatTag = WAVE_FORMAT_PCM; format.nChannels = dma.channels; format.wBitsPerSample = dma.samplebits; format.nSamplesPerSec = dma.speed; format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; format.cbSize = 0; MsgDev( D_NOTE, "DS_CreateBuffers: initialize\n" ); MsgDev( D_NOTE, "DS_CreateBuffers: setting EXCLUSIVE coop level " ); if( DS_OK != pDS->lpVtbl->SetCooperativeLevel( pDS, hInst, DSSCL_EXCLUSIVE )) { MsgDev( D_NOTE, "- failed\n" ); SNDDMA_FreeSound(); return false; } MsgDev( D_NOTE, "- ok\n" ); // get access to the primary buffer, if possible, so we can set the sound hardware format Mem_Set( &dsbuf, 0, sizeof( dsbuf )); dsbuf.dwSize = sizeof( DSBUFFERDESC ); dsbuf.dwFlags = DSBCAPS_PRIMARYBUFFER; dsbuf.dwBufferBytes = 0; dsbuf.lpwfxFormat = NULL; Mem_Set( &dsbcaps, 0, sizeof( dsbcaps )); dsbcaps.dwSize = sizeof( dsbcaps ); primary_format_set = false; MsgDev( D_NOTE, "DS_CreateBuffers: creating primary buffer " ); if( pDS->lpVtbl->CreateSoundBuffer( pDS, &dsbuf, &pDSPBuf, NULL ) == DS_OK ) { pformat = format; MsgDev( D_NOTE, "- ok\n" ); if( snd_firsttime ) MsgDev( D_NOTE, "DS_CreateBuffers: setting primary sound format " ); if( pDSPBuf->lpVtbl->SetFormat( pDSPBuf, &pformat ) != DS_OK ) { if( snd_firsttime ) MsgDev( D_NOTE, "- failed\n" ); } else { if( snd_firsttime ) MsgDev( D_NOTE, "- ok\n" ); primary_format_set = true; } } else MsgDev( D_NOTE, "- failed\n" ); if( !primary_format_set || !s_primary->integer ) { // create the secondary buffer we'll actually work with Mem_Set( &dsbuf, 0, sizeof( dsbuf )); dsbuf.dwSize = sizeof( DSBUFFERDESC ); dsbuf.dwFlags = (DSBCAPS_CTRLFREQUENCY|DSBCAPS_LOCSOFTWARE); dsbuf.dwBufferBytes = SECONDARY_BUFFER_SIZE; dsbuf.lpwfxFormat = &format; Mem_Set( &dsbcaps, 0, sizeof( dsbcaps )); dsbcaps.dwSize = sizeof( dsbcaps ); MsgDev( D_NOTE, "DS_CreateBuffers: creating secondary buffer " ); if( pDS->lpVtbl->CreateSoundBuffer( pDS, &dsbuf, &pDSBuf, NULL ) == DS_OK ) { MsgDev( D_NOTE, "- ok\n" ); } else { // couldn't get hardware, fallback to software. dsbuf.dwFlags = (DSBCAPS_LOCSOFTWARE|DSBCAPS_GETCURRENTPOSITION2); if( pDS->lpVtbl->CreateSoundBuffer( pDS, &dsbuf, &pDSBuf, NULL ) != DS_OK ) { MsgDev( D_NOTE, "- failed\n" ); SNDDMA_FreeSound (); return false; } MsgDev( D_INFO, "- failed. forced to software\n" ); } dma.channels = format.nChannels; dma.samplebits = format.wBitsPerSample; dma.speed = format.nSamplesPerSec; if( pDSBuf->lpVtbl->GetCaps( pDSBuf, &dsbcaps ) != DS_OK ) { MsgDev( D_ERROR, "DS_CreateBuffers: GetCaps failed\n"); SNDDMA_FreeSound (); return false; } MsgDev( D_NOTE, "DS_CreateBuffers: using secondary sound buffer\n" ); } else { MsgDev( D_NOTE, "DS_CreateBuffers: using primary sound buffer\n" ); MsgDev( D_NOTE, "DS_CreateBuffers: setting WRITEPRIMARY coop level " ); if( pDS->lpVtbl->SetCooperativeLevel( pDS, hInst, DSSCL_WRITEPRIMARY ) != DS_OK ) { MsgDev( D_NOTE, "- failed\n" ); SNDDMA_FreeSound (); return false; } MsgDev( D_NOTE, "- ok\n" ); if( pDSPBuf->lpVtbl->GetCaps( pDSPBuf, &dsbcaps ) != DS_OK ) { MsgDev( D_ERROR, "DS_CreateBuffers: GetCaps failed\n"); SNDDMA_FreeSound (); return false; } pDSBuf = pDSPBuf; } // make sure mixer is active if( pDSBuf->lpVtbl->Play( pDSBuf, 0, 0, DSBPLAY_LOOPING ) != DS_OK ) { MsgDev( D_ERROR, "DS_CreateBuffers: looped sound play failed\n" ); SNDDMA_FreeSound (); return false; } // we don't want anyone to access the buffer directly w/o locking it first lpData = NULL; dma.samplepos = 0; snd_hwnd = (HWND)hInst; gSndBufSize = dsbcaps.dwBufferBytes; dma.samples = gSndBufSize / (dma.samplebits / 8 ); dma.submission_chunk = 1; dma.buffer = (byte *)lpData; sample16 = (dma.samplebits / 8) - 1; SNDDMA_BeginPainting(); if( dma.buffer ) Mem_Set( dma.buffer, 0, dma.samples * dma.samplebits / 8 ); SNDDMA_Submit(); return true; } /* ================== DS_DestroyBuffers ================== */ static void DS_DestroyBuffers( void ) { MsgDev( D_NOTE, "DS_DestroyBuffers: shutdown\n" ); if( pDS ) { MsgDev( D_NOTE, "DS_DestroyBuffers: setting NORMAL coop level\n" ); pDS->lpVtbl->SetCooperativeLevel( pDS, snd_hwnd, DSSCL_NORMAL ); } if( pDSBuf ) { MsgDev( D_NOTE, "DS_DestroyBuffers: stopping and releasing sound buffer\n" ); pDSBuf->lpVtbl->Stop( pDSBuf ); pDSBuf->lpVtbl->Release( pDSBuf ); } // only release primary buffer if it's not also the mixing buffer we just released if( pDSPBuf && ( pDSBuf != pDSPBuf )) { MsgDev( D_NOTE, "DS_DestroyBuffers: releasing primary buffer\n" ); pDSPBuf->lpVtbl->Release( pDSPBuf ); } pDSBuf = NULL; pDSPBuf = NULL; dma.buffer = NULL; } /* ================== SNDDMA_FreeSound ================== */ void SNDDMA_FreeSound( void ) { int i; if( pDS ) { DS_DestroyBuffers(); pDS->lpVtbl->Release( pDS ); Sys_FreeLibrary( &dsound_dll ); } if( hWaveOut ) { waveOutReset( hWaveOut ); if( lpWaveHdr ) { for( i = 0; i < WAV_BUFFERS; i++ ) waveOutUnprepareHeader( hWaveOut, lpWaveHdr + i, sizeof( WAVEHDR )); } waveOutClose( hWaveOut ); if( hWaveHdr ) { GlobalUnlock( hWaveHdr ); GlobalFree( hWaveHdr ); } if( hData ) { GlobalUnlock( hData ); GlobalFree( hData ); } } pDS = NULL; pDSBuf = NULL; pDSPBuf = NULL; hWaveOut = 0; hData = 0; hWaveHdr = 0; lpData = NULL; lpWaveHdr = NULL; dsound_init = false; wav_init = false; } /* ================== SNDDMA_InitDirect Direct-Sound support ================== */ si_state_t SNDDMA_InitDirect( void *hInst ) { DSCAPS dscaps; HRESULT hresult; dma.channels = 2; dma.samplebits = 16; switch( s_khz->integer ) { case 44: dma.speed = 44100; break; case 22: dma.speed = 22050; break; default: dma.speed = 11025; break; } if( !dsound_dll.link ) { if( !Sys_LoadLibrary( NULL, &dsound_dll )) return SIS_FAILURE; } MsgDev( D_NOTE, "SNDDMA_InitDirect: creating DS object " ); if(( hresult = iDirectSoundCreate( NULL, &pDS, NULL )) != DS_OK ) { if( hresult != DSERR_ALLOCATED ) { MsgDev( D_NOTE, "- failed\n" ); return SIS_FAILURE; } MsgDev( D_NOTE, "- failed, hardware already in use\n" ); return SIS_NOTAVAIL; } MsgDev( D_NOTE, "- ok\n" ); dscaps.dwSize = sizeof( dscaps ); if( pDS->lpVtbl->GetCaps( pDS, &dscaps ) != DS_OK ) MsgDev( D_ERROR, "SNDDMA_InitDirect: GetCaps failed\n"); if( dscaps.dwFlags & DSCAPS_EMULDRIVER ) { MsgDev( D_ERROR, "SNDDMA_InitDirect: no DSound driver found\n" ); SNDDMA_FreeSound(); return SIS_FAILURE; } if( !DS_CreateBuffers( hInst )) return SIS_FAILURE; dsound_init = true; return SIS_SUCCESS; } /* ================== SNDDM_InitWav Crappy windows multimedia base ================== */ si_state_t SNDDMA_InitWav( void ) { WAVEFORMATEX format; HRESULT hr; int i; snd_sent = 0; snd_completed = 0; dma.channels = 2; dma.samplebits = 16; switch( s_khz->integer ) { case 44: dma.speed = 44100; break; case 22: dma.speed = 22050; break; default: dma.speed = 11025; break; } Mem_Set( &format, 0, sizeof( format )); format.wFormatTag = WAVE_FORMAT_PCM; format.nChannels = dma.channels; format.wBitsPerSample = dma.samplebits; format.nSamplesPerSec = dma.speed; format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; format.cbSize = 0; // open a waveform device for output using window callback. MsgDev( D_NOTE, "SNDDMA_InitWav: initializing wave sound " ); if(( hr = waveOutOpen((LPHWAVEOUT)&hWaveOut, WAVE_MAPPER, &format, 0, 0, CALLBACK_NULL)) != MMSYSERR_NOERROR ) { if( hr != MMSYSERR_ALLOCATED ) { MsgDev( D_NOTE, "- failed\n" ); return SIS_FAILURE; } MsgDev( D_NOTE, "- failed, hardware already in use\n" ); return SIS_NOTAVAIL; } MsgDev( D_NOTE, "- ok\n" ); // allocate and lock memory for the waveform data. The memory // for waveform data must be globally allocated with // GMEM_MOVEABLE and GMEM_SHARE flags. gSndBufSize = WAV_BUFFERS * WAV_BUFFER_SIZE; hData = GlobalAlloc( GMEM_MOVEABLE|GMEM_SHARE, gSndBufSize ); if( !hData ) { SNDDMA_FreeSound(); return SIS_FAILURE; } lpData = GlobalLock( hData ); if( !lpData ) { SNDDMA_FreeSound (); return SIS_FAILURE; } Mem_Set( lpData, 0, gSndBufSize ); // Allocate and lock memory for the header. This memory must // also be globally allocated with GMEM_MOVEABLE and // GMEM_SHARE flags. hWaveHdr = GlobalAlloc( GMEM_MOVEABLE|GMEM_SHARE, (DWORD)sizeof( WAVEHDR ) * WAV_BUFFERS ); if( hWaveHdr == NULL ) { SNDDMA_FreeSound (); return SIS_FAILURE; } lpWaveHdr = (LPWAVEHDR)GlobalLock( hWaveHdr ); if( lpWaveHdr == NULL ) { SNDDMA_FreeSound(); return SIS_FAILURE; } Mem_Set( lpWaveHdr, 0, sizeof( WAVEHDR ) * WAV_BUFFERS ); // After allocation, set up and prepare headers. for( i = 0; i < WAV_BUFFERS; i++ ) { lpWaveHdr[i].dwBufferLength = WAV_BUFFER_SIZE; lpWaveHdr[i].lpData = lpData + i * WAV_BUFFER_SIZE; if( waveOutPrepareHeader( hWaveOut, lpWaveHdr+i, sizeof( WAVEHDR )) != MMSYSERR_NOERROR ) { SNDDMA_FreeSound(); return SIS_FAILURE; } } dma.samplepos = 0; dma.samples = gSndBufSize / ( dma.samplebits / 8 ); dma.submission_chunk = 512; dma.buffer = (byte *)lpData; sample16 = (dma.samplebits / 8) - 1; wav_init = true; return SIS_SUCCESS; } /* ================== SNDDMA_Init Try to find a sound device to mix for. Returns false if nothing is found. ================== */ int SNDDMA_Init( void *hInst ) { si_state_t stat = SIS_FAILURE; // assume DirectSound won't initialize Mem_Set( &dma, 0, sizeof( dma )); s_wavonly = Cvar_Get( "s_wavonly", "0", CVAR_LATCH_AUDIO|CVAR_ARCHIVE, "force to use WaveOutput only" ); dsound_init = wav_init = 0; // init DirectSound if( !s_wavonly->integer ) { if( snd_firsttime || snd_isdirect ) { stat = SNDDMA_InitDirect( hInst ); if( stat == SIS_SUCCESS ) { snd_isdirect = true; if( snd_firsttime ) MsgDev( D_INFO, "Audio: DirectSound\n" ); } else snd_isdirect = false; } } // if DirectSound didn't succeed in initializing, try to initialize // waveOut sound, unless DirectSound failed because the hardware is // already allocated (in which case the user has already chosen not // to have sound) if( !dsound_init && ( stat != SIS_NOTAVAIL )) { if( snd_firsttime || snd_iswave ) { stat = SNDDMA_InitWav(); if( stat == SIS_SUCCESS ) { snd_iswave = true; if( snd_firsttime ) MsgDev( D_INFO, "Audio: WaveOutput\n" ); } else snd_iswave = false; } } snd_buffer_count = 1; if( !dsound_init && !wav_init ) { if( snd_firsttime ) MsgDev( D_ERROR, "SNDDMA_Init: can't initialize sound device\n" ); return false; } snd_firsttime = false; return true; } /* ============== SNDDMA_GetDMAPos return the current sample position (in mono samples read) inside the recirculating dma buffer, so the mixing code will know how many sample are required to fill it up. =============== */ int SNDDMA_GetDMAPos( void ) { int s; if( dsound_init ) { MMTIME mmtime; DWORD dwWrite; mmtime.wType = TIME_SAMPLES; pDSBuf->lpVtbl->GetCurrentPosition( pDSBuf, &mmtime.u.sample, &dwWrite ); s = mmtime.u.sample - mmstarttime.u.sample; } else if( wav_init ) { s = snd_sent * WAV_BUFFER_SIZE; } s >>= sample16; s &= (dma.samples - 1); return s; } /* ============== SNDDMA_GetSoundtime update global soundtime =============== */ int SNDDMA_GetSoundtime( void ) { static int buffers, oldsamplepos; int samplepos, fullsamples; fullsamples = dma.samples / dma.channels; // it is possible to miscount buffers // if it has wrapped twice between // calls to S_Update. Oh well. samplepos = SNDDMA_GetDMAPos(); if( samplepos < oldsamplepos ) { buffers++; // buffer wrapped if( paintedtime > 0x40000000 ) { // time to chop things off to avoid 32 bit limits buffers = 0; paintedtime = fullsamples; S_StopAllSounds(); } } oldsamplepos = samplepos; return (buffers * fullsamples + samplepos / dma.channels); } /* ============== SNDDMA_BeginPainting Makes sure dma.buffer is valid =============== */ void SNDDMA_BeginPainting( void ) { int reps; DWORD dwSize2; DWORD *pbuf, *pbuf2; HRESULT hr; DWORD dwStatus; if( !pDSBuf ) return; // if the buffer was lost or stopped, restore it and/or restart it if( pDSBuf->lpVtbl->GetStatus( pDSBuf, &dwStatus ) != DS_OK ) MsgDev( D_WARN, "SNDDMA_BeginPainting: couldn't get sound buffer status\n" ); if( dwStatus & DSBSTATUS_BUFFERLOST ) pDSBuf->lpVtbl->Restore( pDSBuf ); if(!( dwStatus & DSBSTATUS_PLAYING )) pDSBuf->lpVtbl->Play( pDSBuf, 0, 0, DSBPLAY_LOOPING ); // lock the dsound buffer dma.buffer = NULL; reps = 0; while(( hr = pDSBuf->lpVtbl->Lock( pDSBuf, 0, gSndBufSize, &pbuf, &locksize, &pbuf2, &dwSize2, 0 )) != DS_OK ) { if( hr != DSERR_BUFFERLOST ) { MsgDev( D_ERROR, "SNDDMA_BeginPainting: lock error '%s'\n", DSoundError( hr )); S_Shutdown (); return; } else pDSBuf->lpVtbl->Restore( pDSBuf ); if( ++reps > 2 ) return; } dma.buffer = (byte *)pbuf; } /* ============== SNDDMA_Submit Send sound to device if buffer isn't really the dma buffer Also unlocks the dsound buffer =============== */ void SNDDMA_Submit( void ) { LPWAVEHDR h; int wResult; if( !dma.buffer ) return; // unlock the dsound buffer if( pDSBuf ) pDSBuf->lpVtbl->Unlock( pDSBuf, dma.buffer, locksize, NULL, 0 ); if( !wav_init ) return; // find which sound blocks have completed while( 1 ) { if( snd_completed == snd_sent ) break; if(!( lpWaveHdr[snd_completed & WAV_MASK].dwFlags & WHDR_DONE )) break; snd_completed++; // this buffer has been played } // submit a few new sound blocks while((( snd_sent - snd_completed ) >> sample16 ) < 8 ) { h = lpWaveHdr + ( snd_sent & WAV_MASK ); if( paintedtime / 256 <= snd_sent ) break; snd_sent++; // Now the data block can be sent to the output device. The // waveOutWrite function returns immediately and waveform // data is sent to the output device in the background. wResult = waveOutWrite( hWaveOut, h, sizeof( WAVEHDR )); if( wResult != MMSYSERR_NOERROR ) { MsgDev( D_ERROR, "SNDDMA_Submit: failed to write block to device\n" ); SNDDMA_FreeSound (); return; } } } /* ============== SNDDMA_Shutdown Reset the sound device for exiting =============== */ void SNDDMA_Shutdown( void ) { SNDDMA_FreeSound(); } /* =========== S_PrintDeviceName =========== */ void S_PrintDeviceName( void ) { if( snd_isdirect ) Msg( "Audio: DirectSound\n" ); if( snd_iswave ) Msg( "Audio: WaveOutput\n" ); } /* =========== S_Activate Called when the main window gains or loses focus. The window have been destroyed and recreated between a deactivate and an activate. =========== */ void S_Activate( bool active, void *hInst ) { snd_hwnd = (HWND)hInst; if( active ) { if( pDS && snd_hwnd && snd_isdirect ) DS_CreateBuffers( snd_hwnd ); } else { if( pDS && snd_hwnd && snd_isdirect ) DS_DestroyBuffers(); else if( snd_iswave ) waveOutReset( hWaveOut ); } }