2003-02-16 04:02:21 +00:00
|
|
|
#include "../../global.h"
|
2002-12-13 08:33:25 +00:00
|
|
|
#include "DSoundHelpers.h"
|
2002-12-21 07:21:49 +00:00
|
|
|
#include "../../RageUtil.h"
|
|
|
|
|
#include "../../RageLog.h"
|
2004-06-06 07:23:15 +00:00
|
|
|
#include "archutils/Win32/GetFileInformation.h"
|
2002-12-13 08:33:25 +00:00
|
|
|
|
2004-01-17 05:21:24 +00:00
|
|
|
#if defined(_WINDOWS)
|
2003-11-14 17:17:36 +00:00
|
|
|
#include <mmsystem.h>
|
2004-01-17 05:21:24 +00:00
|
|
|
#endif
|
2003-02-20 21:57:28 +00:00
|
|
|
#define DIRECTSOUND_VERSION 0x0700
|
2002-12-21 07:21:49 +00:00
|
|
|
#include <dsound.h>
|
|
|
|
|
|
2002-12-21 07:37:34 +00:00
|
|
|
#pragma comment(lib, "dsound.lib")
|
|
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
BOOL CALLBACK DSound::EnumCallback( LPGUID lpGuid, LPCSTR lpcstrDescription, LPCSTR lpcstrModule, LPVOID lpContext )
|
2003-02-17 02:37:38 +00:00
|
|
|
{
|
2004-06-06 07:23:15 +00:00
|
|
|
CString sLine = ssprintf( "DirectSound Driver: %s", lpcstrDescription );
|
|
|
|
|
if( lpcstrModule[0] )
|
|
|
|
|
{
|
|
|
|
|
sLine += ssprintf( " (%s", lpcstrModule );
|
|
|
|
|
|
2004-06-20 01:35:25 +00:00
|
|
|
#ifndef _XBOX
|
2004-06-06 07:23:15 +00:00
|
|
|
CString sPath = FindSystemFile( lpcstrModule );
|
|
|
|
|
if( sPath != "" )
|
|
|
|
|
{
|
|
|
|
|
CString ver;
|
|
|
|
|
if( GetFileVersion( sPath, ver ) )
|
|
|
|
|
sLine += ssprintf(" %s", ver.c_str());
|
|
|
|
|
}
|
2004-06-20 01:35:25 +00:00
|
|
|
#endif
|
2004-06-06 07:23:15 +00:00
|
|
|
|
|
|
|
|
sLine += ")";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG->Info( "%s", sLine.c_str() );
|
2003-02-17 02:37:38 +00:00
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
|
}
|
|
|
|
|
|
2003-07-31 00:50:25 +00:00
|
|
|
void DSound::SetPrimaryBufferMode()
|
|
|
|
|
{
|
|
|
|
|
#ifndef _XBOX
|
|
|
|
|
DSBUFFERDESC format;
|
2004-03-05 02:41:43 +00:00
|
|
|
memset( &format, 0, sizeof(format) );
|
2003-07-31 00:50:25 +00:00
|
|
|
format.dwSize = sizeof(format);
|
|
|
|
|
format.dwFlags = DSBCAPS_PRIMARYBUFFER;
|
|
|
|
|
format.dwBufferBytes = 0;
|
|
|
|
|
format.lpwfxFormat = NULL;
|
|
|
|
|
|
|
|
|
|
IDirectSoundBuffer *buf;
|
|
|
|
|
HRESULT hr = this->GetDS()->CreateSoundBuffer(&format, &buf, NULL);
|
|
|
|
|
/* hr */
|
|
|
|
|
if( FAILED(hr) )
|
|
|
|
|
{
|
|
|
|
|
LOG->Warn(hr_ssprintf(hr, "Couldn't create primary buffer"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WAVEFORMATEX waveformat;
|
2004-08-14 19:53:15 +00:00
|
|
|
memset( &waveformat, 0, sizeof(waveformat) );
|
2003-07-31 00:50:25 +00:00
|
|
|
waveformat.cbSize = 0;
|
|
|
|
|
waveformat.wFormatTag = WAVE_FORMAT_PCM;
|
2004-08-14 19:53:15 +00:00
|
|
|
waveformat.wBitsPerSample = 16;
|
|
|
|
|
waveformat.nChannels = 2;
|
|
|
|
|
waveformat.nSamplesPerSec = 44100;
|
|
|
|
|
waveformat.nBlockAlign = 4;
|
|
|
|
|
waveformat.nAvgBytesPerSec = waveformat.nSamplesPerSec * waveformat.nBlockAlign;
|
2003-07-31 00:50:25 +00:00
|
|
|
|
|
|
|
|
// Set the primary buffer's format
|
|
|
|
|
hr = IDirectSoundBuffer_SetFormat( buf, &waveformat );
|
|
|
|
|
if( FAILED(hr) )
|
2004-03-05 02:41:43 +00:00
|
|
|
LOG->Warn( hr_ssprintf(hr, "SetFormat on primary buffer") );
|
2003-07-31 00:50:25 +00:00
|
|
|
|
2004-08-14 19:53:15 +00:00
|
|
|
DWORD got;
|
|
|
|
|
hr = buf->GetFormat( &waveformat, sizeof(waveformat), &got );
|
|
|
|
|
if( FAILED(hr) )
|
|
|
|
|
LOG->Warn( hr_ssprintf(hr, "GetFormat on primary buffer") );
|
|
|
|
|
else if( waveformat.nSamplesPerSec != 44100 )
|
2004-08-26 04:36:35 +00:00
|
|
|
LOG->Warn( "Primary buffer set to %i instead of 44100", waveformat.nSamplesPerSec );
|
2004-08-14 19:53:15 +00:00
|
|
|
|
2003-07-31 01:11:42 +00:00
|
|
|
/* MS docs:
|
|
|
|
|
*
|
|
|
|
|
* When there are no sounds playing, DirectSound stops the mixer engine and halts DMA
|
|
|
|
|
* (direct memory access) activity. If your application has frequent short intervals of
|
|
|
|
|
* silence, the overhead of starting and stopping the mixer each time a sound is played
|
|
|
|
|
* may be worse than the DMA overhead if you kept the mixer active. Also, some sound
|
|
|
|
|
* hardware or drivers may produce unwanted audible artifacts from frequent starting and
|
|
|
|
|
* stopping of playback. If your application is playing audio almost continuously with only
|
|
|
|
|
* short breaks of silence, you can force the mixer engine to remain active by calling the
|
|
|
|
|
* IDirectSoundBuffer::Play method for the primary buffer. The mixer will continue to run
|
|
|
|
|
* silently.
|
|
|
|
|
*
|
|
|
|
|
* However, I just added the above code and I don't want to change more until it's tested.
|
|
|
|
|
*/
|
|
|
|
|
// buf->Play(0, 0, DSBPLAY_LOOPING);
|
|
|
|
|
|
2003-07-31 00:50:25 +00:00
|
|
|
buf->Release();
|
|
|
|
|
#endif
|
|
|
|
|
}
|
2003-02-17 02:37:38 +00:00
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
DSound::DSound()
|
2002-12-13 08:33:25 +00:00
|
|
|
{
|
2002-12-21 07:21:49 +00:00
|
|
|
HRESULT hr;
|
2002-12-13 08:33:25 +00:00
|
|
|
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2003-02-20 07:17:15 +00:00
|
|
|
// Initialize COM
|
2003-02-20 10:41:43 +00:00
|
|
|
if( FAILED( hr = CoInitialize( NULL ) ) )
|
2003-02-20 07:17:15 +00:00
|
|
|
RageException::ThrowNonfatal(hr_ssprintf(hr, "CoInitialize"));
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2003-02-20 07:17:15 +00:00
|
|
|
|
|
|
|
|
// Create IDirectSound using the primary sound device
|
|
|
|
|
if( FAILED( hr = DirectSoundCreate( NULL, &ds, NULL ) ) )
|
|
|
|
|
RageException::ThrowNonfatal(hr_ssprintf(hr, "DirectSoundCreate"));
|
|
|
|
|
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2004-03-26 06:39:29 +00:00
|
|
|
static bool bShownInfo = false;
|
|
|
|
|
if( !bShownInfo )
|
|
|
|
|
{
|
|
|
|
|
bShownInfo = true;
|
|
|
|
|
DirectSoundEnumerate(EnumCallback, 0);
|
2004-03-26 06:48:36 +00:00
|
|
|
|
|
|
|
|
DSCAPS Caps;
|
|
|
|
|
Caps.dwSize = sizeof(Caps);
|
|
|
|
|
HRESULT hr;
|
|
|
|
|
if( FAILED(hr = ds->GetCaps(&Caps)) )
|
|
|
|
|
{
|
|
|
|
|
LOG->Warn( hr_ssprintf(hr, "ds->GetCaps failed") );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
LOG->Info( "DirectSound sample rates: %i..%i %s", Caps.dwMinSecondarySampleRate, Caps.dwMaxSecondarySampleRate,
|
|
|
|
|
(Caps.dwFlags & DSCAPS_CONTINUOUSRATE)?"(continuous)":"" );
|
|
|
|
|
}
|
2004-03-26 06:39:29 +00:00
|
|
|
}
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
/* Try to set primary mixing privileges */
|
2004-03-05 02:41:43 +00:00
|
|
|
hr = ds->SetCooperativeLevel( GetDesktopWindow(), DSSCL_PRIORITY );
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2003-07-31 00:50:25 +00:00
|
|
|
|
|
|
|
|
SetPrimaryBufferMode();
|
2002-12-13 08:33:25 +00:00
|
|
|
}
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
DSound::~DSound()
|
2002-12-13 08:33:25 +00:00
|
|
|
{
|
2003-02-20 07:17:15 +00:00
|
|
|
ds->Release();
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2003-02-20 07:17:15 +00:00
|
|
|
CoUninitialize();
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DSound::IsEmulated() const
|
|
|
|
|
{
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2002-12-21 07:21:49 +00:00
|
|
|
/* Don't bother wasting time trying to create buffers if we're
|
|
|
|
|
* emulated. This also gives us better diagnostic information. */
|
|
|
|
|
DSCAPS Caps;
|
|
|
|
|
Caps.dwSize = sizeof(Caps);
|
|
|
|
|
HRESULT hr;
|
2004-03-05 02:41:43 +00:00
|
|
|
if( FAILED(hr = ds->GetCaps(&Caps)) )
|
2002-12-21 07:21:49 +00:00
|
|
|
{
|
2004-03-05 02:41:43 +00:00
|
|
|
LOG->Warn( hr_ssprintf(hr, "ds->GetCaps failed") );
|
2002-12-21 07:21:49 +00:00
|
|
|
/* This is strange, so let's be conservative. */
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return !!(Caps.dwFlags & DSCAPS_EMULDRIVER);
|
2003-07-11 03:15:28 +00:00
|
|
|
#else
|
|
|
|
|
return false;
|
|
|
|
|
#endif
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
|
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
DSoundBuf::DSoundBuf( DSound &ds, DSoundBuf::hw hardware,
|
|
|
|
|
int channels_, int samplerate_, int samplebits_, int writeahead_ )
|
2002-12-21 07:21:49 +00:00
|
|
|
{
|
|
|
|
|
channels = channels_;
|
|
|
|
|
samplerate = samplerate_;
|
|
|
|
|
samplebits = samplebits_;
|
2004-02-15 04:34:51 +00:00
|
|
|
writeahead = writeahead_ * bytes_per_frame();
|
2004-09-05 06:05:56 +00:00
|
|
|
volume = -1; /* unset */
|
2002-12-21 07:21:49 +00:00
|
|
|
buffer_locked = false;
|
2004-01-19 22:21:57 +00:00
|
|
|
last_cursor_pos = write_cursor = buffer_bytes_filled = 0;
|
|
|
|
|
LastPosition = 0;
|
2004-02-27 06:47:07 +00:00
|
|
|
playing = false;
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2002-12-31 01:08:35 +00:00
|
|
|
/* The size of the actual DSound buffer. This can be large; we generally
|
|
|
|
|
* won't fill it completely. */
|
|
|
|
|
buffersize = 1024*64;
|
2004-01-22 23:04:59 +00:00
|
|
|
buffersize = max( buffersize, writeahead );
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2002-12-13 08:33:25 +00:00
|
|
|
WAVEFORMATEX waveformat;
|
2004-03-05 02:41:43 +00:00
|
|
|
memset( &waveformat, 0, sizeof(waveformat) );
|
2004-08-14 19:53:15 +00:00
|
|
|
waveformat.cbSize = 0;
|
2002-12-13 08:33:25 +00:00
|
|
|
waveformat.wFormatTag = WAVE_FORMAT_PCM;
|
|
|
|
|
|
2003-04-25 04:10:18 +00:00
|
|
|
bool NeedCtrlFrequency = false;
|
2004-03-05 02:41:43 +00:00
|
|
|
if( samplerate == DYNAMIC_SAMPLERATE )
|
2003-04-25 04:10:18 +00:00
|
|
|
{
|
|
|
|
|
samplerate = 44100;
|
|
|
|
|
NeedCtrlFrequency = true;
|
|
|
|
|
}
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
int bytes = samplebits/8;
|
|
|
|
|
waveformat.wBitsPerSample = WORD(samplebits);
|
2002-12-13 08:33:25 +00:00
|
|
|
waveformat.nChannels = WORD(channels);
|
|
|
|
|
waveformat.nSamplesPerSec = DWORD(samplerate);
|
|
|
|
|
waveformat.nBlockAlign = WORD(bytes*channels);
|
|
|
|
|
waveformat.nAvgBytesPerSec = samplerate * bytes*channels;
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
/* Try to create the secondary buffer */
|
|
|
|
|
DSBUFFERDESC format;
|
2004-03-05 02:41:43 +00:00
|
|
|
memset( &format, 0, sizeof(format) );
|
2002-12-21 07:21:49 +00:00
|
|
|
format.dwSize = sizeof(format);
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifdef _XBOX
|
2003-11-13 00:39:36 +00:00
|
|
|
format.dwFlags = 0;
|
2003-07-11 03:15:28 +00:00
|
|
|
#else
|
2003-01-19 19:55:24 +00:00
|
|
|
format.dwFlags = DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLVOLUME;
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifndef _XBOX
|
2003-01-08 01:36:06 +00:00
|
|
|
/* Don't use DSBCAPS_STATIC. It's meant for static buffers, and we
|
|
|
|
|
* only use streaming buffers. */
|
2004-03-05 02:41:43 +00:00
|
|
|
if( hardware == HW_HARDWARE )
|
2002-12-21 07:21:49 +00:00
|
|
|
format.dwFlags |= DSBCAPS_LOCHARDWARE;
|
2003-01-08 01:36:06 +00:00
|
|
|
else
|
2002-12-21 07:21:49 +00:00
|
|
|
format.dwFlags |= DSBCAPS_LOCSOFTWARE;
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2003-01-19 19:55:24 +00:00
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
if( NeedCtrlFrequency )
|
2003-04-25 04:10:18 +00:00
|
|
|
format.dwFlags |= DSBCAPS_CTRLFREQUENCY;
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
format.dwBufferBytes = buffersize;
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2002-12-21 07:21:49 +00:00
|
|
|
format.dwReserved = 0;
|
2003-11-13 00:39:36 +00:00
|
|
|
#else
|
2004-03-05 02:41:43 +00:00
|
|
|
DSMIXBINVOLUMEPAIR dsmbvp[8] =
|
|
|
|
|
{
|
|
|
|
|
{ DSMIXBIN_FRONT_LEFT, DSBVOLUME_MAX}, // left channel
|
|
|
|
|
{ DSMIXBIN_FRONT_RIGHT, DSBVOLUME_MAX}, // right channel
|
|
|
|
|
{ DSMIXBIN_FRONT_CENTER, DSBVOLUME_MAX}, // left channel
|
|
|
|
|
{ DSMIXBIN_FRONT_CENTER, DSBVOLUME_MAX}, // right channel
|
|
|
|
|
{ DSMIXBIN_BACK_LEFT, DSBVOLUME_MAX}, // left channel
|
|
|
|
|
{ DSMIXBIN_BACK_RIGHT, DSBVOLUME_MAX}, // right channel
|
|
|
|
|
{ DSMIXBIN_LOW_FREQUENCY, DSBVOLUME_MAX}, // left channel
|
|
|
|
|
{ DSMIXBIN_LOW_FREQUENCY, DSBVOLUME_MAX} // right channel
|
|
|
|
|
};
|
2003-11-13 00:39:36 +00:00
|
|
|
DSMIXBINS dsmb;
|
|
|
|
|
dsmb.dwMixBinCount = 8;
|
|
|
|
|
dsmb.lpMixBinVolumePairs = dsmbvp;
|
|
|
|
|
|
|
|
|
|
format.lpMixBins = &dsmb;
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2003-11-13 00:39:36 +00:00
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
format.lpwfxFormat = &waveformat;
|
|
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
HRESULT hr = ds.GetDS()->CreateSoundBuffer( &format, &buf, NULL );
|
|
|
|
|
if( FAILED(hr) )
|
|
|
|
|
RageException::ThrowNonfatal( hr_ssprintf(hr, "CreateSoundBuffer failed") );
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2003-01-08 01:36:06 +00:00
|
|
|
/* I'm not sure this should ever be needed, but ... */
|
|
|
|
|
DSBCAPS bcaps;
|
|
|
|
|
bcaps.dwSize=sizeof(bcaps);
|
|
|
|
|
hr = buf->GetCaps(&bcaps);
|
2004-03-05 02:41:43 +00:00
|
|
|
if( FAILED(hr) )
|
2003-01-08 01:36:06 +00:00
|
|
|
RageException::Throw(hr_ssprintf(hr, "buf->GetCaps"));
|
2004-03-05 02:41:43 +00:00
|
|
|
if( int(bcaps.dwBufferBytes) != buffersize )
|
2003-01-08 01:36:06 +00:00
|
|
|
{
|
2004-03-05 02:41:43 +00:00
|
|
|
LOG->Warn( "bcaps.dwBufferBytes (%i) != buffersize(%i); adjusting", bcaps.dwBufferBytes, buffersize );
|
2003-01-08 01:36:06 +00:00
|
|
|
buffersize = bcaps.dwBufferBytes;
|
2004-03-05 02:41:43 +00:00
|
|
|
writeahead = min( writeahead, buffersize );
|
2003-01-08 01:36:06 +00:00
|
|
|
}
|
2004-08-27 04:53:00 +00:00
|
|
|
|
|
|
|
|
if( !(bcaps.dwFlags & DSBCAPS_CTRLVOLUME) )
|
|
|
|
|
LOG->Warn( "Sound channel missing DSBCAPS_CTRLVOLUME" );
|
|
|
|
|
if( !(bcaps.dwFlags & DSBCAPS_GETCURRENTPOSITION2) )
|
|
|
|
|
LOG->Warn( "Sound channel missing DSBCAPS_GETCURRENTPOSITION2" );
|
|
|
|
|
|
|
|
|
|
DWORD got;
|
|
|
|
|
hr = buf->GetFormat( &waveformat, sizeof(waveformat), &got );
|
|
|
|
|
if( FAILED(hr) )
|
|
|
|
|
LOG->Warn( hr_ssprintf(hr, "GetFormat on secondary buffer") );
|
|
|
|
|
else if( (int) waveformat.nSamplesPerSec != samplerate )
|
|
|
|
|
LOG->Warn( "Secondary buffer set to %i instead of %i", waveformat.nSamplesPerSec, samplerate );
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2003-01-19 19:55:24 +00:00
|
|
|
}
|
|
|
|
|
|
2003-04-25 04:10:18 +00:00
|
|
|
void DSoundBuf::SetSampleRate(int hz)
|
|
|
|
|
{
|
|
|
|
|
samplerate = hz;
|
2004-03-05 02:41:43 +00:00
|
|
|
HRESULT hr = buf->SetFrequency( hz );
|
|
|
|
|
if( FAILED(hr) )
|
|
|
|
|
RageException::Throw( hr_ssprintf(hr, "buf->SetFrequency(%i)", hz) );
|
2003-04-25 04:10:18 +00:00
|
|
|
}
|
|
|
|
|
|
2003-01-19 19:55:24 +00:00
|
|
|
void DSoundBuf::SetVolume(float vol)
|
|
|
|
|
{
|
|
|
|
|
ASSERT(vol >= 0);
|
|
|
|
|
ASSERT(vol <= 1);
|
2003-02-19 23:49:57 +00:00
|
|
|
|
|
|
|
|
if( vol == 0 )
|
|
|
|
|
vol = 0.001f; // fix log10f(0) == -INF
|
2003-01-08 00:44:17 +00:00
|
|
|
float vl2 = log10f(vol) / log10f(2); /* vol log 2 */
|
|
|
|
|
|
2004-08-27 04:53:00 +00:00
|
|
|
/* Volume is a multiplier; SetVolume wants attenuation in hundredths of a decibel. */
|
2004-09-05 06:05:56 +00:00
|
|
|
const int new_volume = max( int(1000 * vl2), DSBVOLUME_MIN );
|
2004-01-22 06:55:18 +00:00
|
|
|
|
|
|
|
|
if( volume == new_volume )
|
|
|
|
|
return;
|
|
|
|
|
|
2004-09-05 05:55:36 +00:00
|
|
|
HRESULT hr = buf->SetVolume( new_volume );
|
2004-09-05 01:49:42 +00:00
|
|
|
if( FAILED(hr) )
|
|
|
|
|
{
|
|
|
|
|
static bool bWarned = false;
|
|
|
|
|
if( !bWarned )
|
|
|
|
|
LOG->Warn( hr_ssprintf(hr, "DirectSoundBuffer::SetVolume(%i) failed", volume) );
|
|
|
|
|
bWarned = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
volume = new_volume;
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
|
|
|
|
|
2003-01-31 21:34:15 +00:00
|
|
|
/* Determine if "pos" is between "start" and "end", for a circular buffer. */
|
2004-03-05 02:41:43 +00:00
|
|
|
static bool contained( int start, int end, int pos )
|
2003-01-31 21:34:15 +00:00
|
|
|
{
|
2004-03-05 02:41:43 +00:00
|
|
|
if( end >= start ) /* start ... pos ... end */
|
2003-01-31 21:34:15 +00:00
|
|
|
return start <= pos && pos <= end;
|
|
|
|
|
else
|
|
|
|
|
return pos >= start || pos <= end;
|
|
|
|
|
}
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
DSoundBuf::~DSoundBuf()
|
|
|
|
|
{
|
|
|
|
|
buf->Release();
|
|
|
|
|
}
|
|
|
|
|
|
2004-09-07 00:33:54 +00:00
|
|
|
/* Figure out if we've underrun, and act if appropriate. */
|
|
|
|
|
void DSoundBuf::CheckUnderrun( int cursorstart, int cursorend, int chunksize )
|
|
|
|
|
{
|
|
|
|
|
/* XXX We can figure out if we've underrun, and increase the write-ahead
|
|
|
|
|
* when it happens. Two problems:
|
|
|
|
|
* 1. It's ugly to wait until we actually underrun. (We could store the
|
|
|
|
|
* write-ahead, though.)
|
|
|
|
|
* 2. We don't want a random underrun (eg. virus scanner starts) to
|
|
|
|
|
* permanently increase our write-ahead. We want the smallest possible
|
|
|
|
|
* that will give us reliable audio in normal conditions. I'm not sure
|
|
|
|
|
* of a robust way to do this.
|
|
|
|
|
*
|
|
|
|
|
* Also, writeahead should be a static (all buffers write ahead the same
|
|
|
|
|
* amount); writeahead in the ctor should be a hint only (initial value),
|
|
|
|
|
* and the sound driver should query a sound to get the current writeahead
|
|
|
|
|
* in GetLatencySeconds().
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/* If the buffer is full, we can't be underrunning. */
|
|
|
|
|
if( buffer_bytes_filled >= buffersize )
|
|
|
|
|
return;
|
|
|
|
|
|
2004-09-07 22:57:53 +00:00
|
|
|
/* If nothing is expected to be filled, we can't underrun. */
|
|
|
|
|
if( cursorstart == cursorend )
|
|
|
|
|
return;
|
|
|
|
|
|
2004-09-07 00:33:54 +00:00
|
|
|
/* If there's no data in the buffer at all, then we've completely underrun. Our
|
|
|
|
|
* write cursor is irrelevant; we might be unrelated to the play position completely.
|
|
|
|
|
* Realign. */
|
|
|
|
|
if( buffer_bytes_filled == 0 )
|
|
|
|
|
{
|
|
|
|
|
/* There's no data filled at all. We've completely underrun. */
|
|
|
|
|
/* XXX */
|
|
|
|
|
|
|
|
|
|
int missed_by = cursorend - write_cursor;
|
|
|
|
|
wrap( missed_by, buffersize );
|
|
|
|
|
int first_byte_filled = write_cursor-buffer_bytes_filled;
|
|
|
|
|
wrap( first_byte_filled, buffersize );
|
|
|
|
|
|
|
|
|
|
LOG->Trace("major underrun: %i..%i filled but cursor at %i..%i (missed it by %i) %i/%i",
|
|
|
|
|
first_byte_filled, write_cursor, cursorstart, cursorend,
|
|
|
|
|
missed_by, buffer_bytes_filled, buffersize);
|
|
|
|
|
|
|
|
|
|
write_cursor = cursorstart;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Invariant: the filled region now starts at the beginning of the play region.
|
|
|
|
|
* The "Update buffer_bytes_filled" logic, combined with the above empty buffer
|
|
|
|
|
* check, guarantees this.
|
|
|
|
|
*
|
|
|
|
|
* This is important, so let's consider all cases as of the start of get_output_buf:
|
|
|
|
|
*
|
|
|
|
|
* 1: ....ffff....pppp... (no overlap)
|
|
|
|
|
* invalid: buffer_bytes_filled should have been set to 0.
|
|
|
|
|
*
|
|
|
|
|
* 2: ....ffff.... (overlap, filled is earlier)
|
|
|
|
|
* ..pppp......
|
|
|
|
|
* invalid: buffer_bytes_filled should have been set to 2, resulting in:
|
|
|
|
|
* ......ff....
|
|
|
|
|
* ...pppp.....
|
|
|
|
|
*
|
|
|
|
|
* 3: ......ffff.. (overlap, filled is later)
|
|
|
|
|
* ....pppp....
|
|
|
|
|
* invalid: This case can only happen if the play cursor has wrapped all the
|
|
|
|
|
* way around, in which case buffer_bytes_filled should have been set to 0.
|
|
|
|
|
*
|
|
|
|
|
* 4: ....ffff.... valid
|
|
|
|
|
* ....pppp....
|
|
|
|
|
* 5: ....ff...... valid
|
|
|
|
|
* ....pppp....
|
|
|
|
|
* 6: ............ valid (no data filled)
|
|
|
|
|
* ....pppp....
|
|
|
|
|
*/
|
|
|
|
|
int first_byte_filled = write_cursor-buffer_bytes_filled;
|
|
|
|
|
wrap( first_byte_filled, buffersize );
|
|
|
|
|
if( first_byte_filled != cursorstart )
|
|
|
|
|
{
|
|
|
|
|
LOG->Trace("%i..%i filled but cursor at %i..%i (%i max)",
|
|
|
|
|
first_byte_filled, write_cursor, cursorstart, cursorend, buffer_bytes_filled );
|
|
|
|
|
FAIL_M( "DSoundBuf::CheckUnderrun internal error" );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if( contained(first_byte_filled, write_cursor, cursorend) )
|
|
|
|
|
{
|
|
|
|
|
/* The end of the play cursor has data. We haven't underrun (case #4). */
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* We've underrun. Let's figure out whether, if we continue filling, we'll fill
|
|
|
|
|
* the buffer enough to stop underrunning. This is a little ugly, since we need
|
|
|
|
|
* to simulate. */
|
|
|
|
|
int fake_buffer_bytes_filled = buffer_bytes_filled;
|
|
|
|
|
int fake_write_cursor = write_cursor;
|
|
|
|
|
while(1)
|
|
|
|
|
{
|
|
|
|
|
if( fake_buffer_bytes_filled > writeahead )
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
int num_bytes_empty = writeahead-fake_buffer_bytes_filled;
|
|
|
|
|
if( num_bytes_empty < chunksize )
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
num_bytes_empty = min(num_bytes_empty, buffersize - fake_write_cursor);
|
|
|
|
|
// num_bytes_empty = min(num_bytes_empty, chunksize);
|
|
|
|
|
|
|
|
|
|
fake_buffer_bytes_filled += num_bytes_empty;
|
|
|
|
|
fake_write_cursor += num_bytes_empty;
|
|
|
|
|
wrap( fake_write_cursor, buffersize );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG->Trace("write_cursor %i, fake_write_cursor %i, fake_buffer_bytes_filled %i",
|
|
|
|
|
write_cursor, fake_write_cursor, fake_buffer_bytes_filled );
|
|
|
|
|
|
|
|
|
|
bool bCanCatchUp = contained(first_byte_filled, fake_write_cursor, cursorend);
|
|
|
|
|
/*
|
|
|
|
|
* If bCanCatchUp is false, then based on our writeahead and the chunksize, we'll
|
|
|
|
|
* never fill the buffer. This isn't fuzzy; we simply aren't filling enough, and
|
|
|
|
|
* we need to increase the writeahead.
|
|
|
|
|
*
|
|
|
|
|
* If bCanCatchUp is true, we simply fell behind and we can catch up. If this
|
|
|
|
|
* happens repeatedly, then while the writeahead is sufficient for the prefetch,
|
|
|
|
|
* the scheduler isn't keeping up, and we probably need to increase the writeahead.
|
|
|
|
|
*
|
|
|
|
|
* This is a tricky support issue. We don't want to have to adjust the writeahead
|
|
|
|
|
* dynamically if we can help it, since that means we've already underrun. It's
|
|
|
|
|
* much better to have a properly tuned writeahead for all systems to begin with.
|
|
|
|
|
* I'd much prefer to receive bug reports when the writeahead wasn't enough, so we
|
|
|
|
|
* can figure out the correct writeahead and use it by default.
|
|
|
|
|
*
|
|
|
|
|
* Also, unless we write the writeahead to a preference to keep it long-term (which
|
|
|
|
|
* I'm wary of doing), we'll go through this every game.
|
|
|
|
|
*/
|
|
|
|
|
if( bCanCatchUp )
|
|
|
|
|
{
|
|
|
|
|
/* If we simply continue, we'll catch up. We'll probably have an audible
|
|
|
|
|
* glitch, but we can't prevent that. However, we can probably avoid an
|
|
|
|
|
* arrow skip. */
|
|
|
|
|
/* XXX: if this happens repeatedly over a period of time, increase writeahead */
|
|
|
|
|
int missed_by = cursorend - write_cursor;
|
|
|
|
|
wrap( missed_by, buffersize );
|
|
|
|
|
LOG->Trace("minor underrun: %i..%i filled but cursor at %i..%i (missed it by %i) %i/%i",
|
|
|
|
|
first_byte_filled, write_cursor, cursorstart, cursorend,
|
|
|
|
|
missed_by, buffer_bytes_filled, buffersize);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Based on our writeahead and the chunksize, we'll never fill the buffer. We
|
|
|
|
|
* need to increase the writeahead.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
int prefetch = cursorend - cursorstart;
|
|
|
|
|
wrap( prefetch, buffersize );
|
|
|
|
|
|
|
|
|
|
int old_writeahead = writeahead;
|
|
|
|
|
writeahead = writeahead * 4 / 3;
|
|
|
|
|
/* Snap to bytes_per_frame. */
|
|
|
|
|
writeahead = (writeahead / bytes_per_frame()) * bytes_per_frame();
|
|
|
|
|
writeahead = min( writeahead, buffersize );
|
|
|
|
|
|
2004-09-09 00:20:45 +00:00
|
|
|
LOG->Trace("insufficient writeahead: wants %i (cursor at %i..%i), but we'll only fill to %i; writeahead adjusted from %i to %i",
|
|
|
|
|
prefetch/bytes_per_frame(), cursorstart, cursorend,
|
|
|
|
|
fake_buffer_bytes_filled/bytes_per_frame(), old_writeahead/bytes_per_frame(), writeahead/bytes_per_frame() );
|
2004-09-07 00:33:54 +00:00
|
|
|
}
|
|
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
bool DSoundBuf::get_output_buf( char **buffer, unsigned *bufsiz, int chunksize )
|
2002-12-21 07:21:49 +00:00
|
|
|
{
|
|
|
|
|
ASSERT(!buffer_locked);
|
|
|
|
|
|
2004-02-15 04:34:51 +00:00
|
|
|
chunksize *= bytes_per_frame();
|
|
|
|
|
|
2004-01-31 02:36:10 +00:00
|
|
|
DWORD cursorstart, cursorend;
|
2002-12-21 07:21:49 +00:00
|
|
|
|
|
|
|
|
HRESULT result;
|
|
|
|
|
|
2003-01-31 21:34:15 +00:00
|
|
|
/* It's easiest to think of the cursor as a block, starting and ending at
|
|
|
|
|
* the two values returned by GetCurrentPosition, that we can't write to. */
|
2004-03-05 02:41:43 +00:00
|
|
|
result = buf->GetCurrentPosition( &cursorstart, &cursorend );
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2004-02-27 06:47:07 +00:00
|
|
|
if ( result == DSERR_BUFFERLOST )
|
|
|
|
|
{
|
2002-12-21 07:21:49 +00:00
|
|
|
buf->Restore();
|
2004-03-05 02:41:43 +00:00
|
|
|
result = buf->GetCurrentPosition( &cursorstart, &cursorend );
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
2004-02-27 06:47:07 +00:00
|
|
|
if ( result != DS_OK )
|
|
|
|
|
{
|
2004-03-05 02:41:43 +00:00
|
|
|
LOG->Warn( hr_ssprintf(result, "DirectSound::GetCurrentPosition failed") );
|
2002-12-21 07:21:49 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2004-02-27 06:47:07 +00:00
|
|
|
/* Some cards (Creative AudioPCI) have a no-write area even when not playing. I'm not
|
|
|
|
|
* sure what that means, but it breaks the assumption that we can fill the whole writeahead
|
|
|
|
|
* when prebuffering. */
|
|
|
|
|
if( !playing )
|
|
|
|
|
cursorend = cursorstart;
|
|
|
|
|
|
2004-01-22 23:04:59 +00:00
|
|
|
/* Update buffer_bytes_filled. */
|
|
|
|
|
{
|
|
|
|
|
int first_byte_filled = write_cursor-buffer_bytes_filled;
|
2004-09-07 00:33:54 +00:00
|
|
|
wrap( first_byte_filled, buffersize );
|
2004-01-22 23:04:59 +00:00
|
|
|
|
|
|
|
|
/* The number of bytes that have been played since the last time we got here: */
|
2004-02-25 00:06:52 +00:00
|
|
|
int bytes_played = cursorstart - first_byte_filled;
|
2004-09-07 00:33:54 +00:00
|
|
|
wrap( bytes_played, buffersize );
|
2004-02-25 00:06:52 +00:00
|
|
|
|
2004-01-22 23:04:59 +00:00
|
|
|
buffer_bytes_filled -= bytes_played;
|
2004-02-25 00:06:52 +00:00
|
|
|
buffer_bytes_filled = max( 0, buffer_bytes_filled );
|
2004-01-22 23:04:59 +00:00
|
|
|
}
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2004-09-07 00:33:54 +00:00
|
|
|
CheckUnderrun( cursorstart, cursorend, chunksize );
|
2003-01-31 21:34:15 +00:00
|
|
|
|
|
|
|
|
/* If we already have enough bytes written ahead, stop. */
|
2004-03-05 02:41:43 +00:00
|
|
|
if( buffer_bytes_filled > writeahead )
|
2002-12-31 01:08:35 +00:00
|
|
|
return false;
|
|
|
|
|
|
2004-01-22 23:04:59 +00:00
|
|
|
int num_bytes_empty = writeahead-buffer_bytes_filled;
|
2003-01-31 21:34:15 +00:00
|
|
|
|
|
|
|
|
/* num_bytes_empty is the amount of free buffer space. If it's
|
2002-12-21 07:21:49 +00:00
|
|
|
* too small, come back later. */
|
2004-03-05 02:41:43 +00:00
|
|
|
if( num_bytes_empty < chunksize )
|
2002-12-21 07:21:49 +00:00
|
|
|
return false;
|
|
|
|
|
|
2003-01-31 21:34:15 +00:00
|
|
|
/* I don't want to deal with DSound's split-circular-buffer locking stuff, so clamp
|
2002-12-21 07:21:49 +00:00
|
|
|
* the writing space at the end of the physical buffer. */
|
|
|
|
|
num_bytes_empty = min(num_bytes_empty, buffersize - write_cursor);
|
|
|
|
|
|
|
|
|
|
/* Don't fill more than one chunk at a time. This reduces the maximum
|
|
|
|
|
* amount of time until we give data; that way, if we're short on time,
|
|
|
|
|
* we'll give some data soon instead of lots of data later. */
|
2004-09-07 00:33:54 +00:00
|
|
|
/* Let's not do this; treat chunksize as a "min bytes to fill" above (so we're not
|
|
|
|
|
* constantly filling in a few frames at a time), but not "max bytes to fill". This
|
|
|
|
|
* reduces cases where we don't fill the buffer as much as we should, and in practice
|
|
|
|
|
* makes the "increase the writeahead" logic work much better. */
|
|
|
|
|
// num_bytes_empty = min(num_bytes_empty, chunksize);
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2003-01-31 21:34:15 +00:00
|
|
|
// LOG->Trace("gave %i at %i (%i, %i) %i filled", num_bytes_empty, write_cursor, cursor, write, buffer_bytes_filled);
|
2002-12-31 01:08:35 +00:00
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
/* Lock the audio buffer. */
|
2003-11-13 00:39:36 +00:00
|
|
|
#ifdef _XBOX
|
2004-03-05 02:41:43 +00:00
|
|
|
result = buf->Lock( write_cursor, num_bytes_empty, (LPVOID *)buffer, (DWORD *) bufsiz, NULL, NULL, 0 );
|
2003-11-13 00:39:36 +00:00
|
|
|
#else
|
2004-02-07 20:50:42 +00:00
|
|
|
DWORD junk;
|
2004-03-05 02:41:43 +00:00
|
|
|
result = buf->Lock( write_cursor, num_bytes_empty, (LPVOID *)buffer, (DWORD *) bufsiz, NULL, &junk, 0 );
|
2003-11-13 00:39:36 +00:00
|
|
|
#endif
|
|
|
|
|
|
2003-07-11 03:15:28 +00:00
|
|
|
#ifndef _XBOX
|
2004-03-05 02:41:43 +00:00
|
|
|
if ( result == DSERR_BUFFERLOST )
|
|
|
|
|
{
|
2002-12-21 07:21:49 +00:00
|
|
|
buf->Restore();
|
2004-03-05 02:41:43 +00:00
|
|
|
result = buf->Lock( write_cursor, num_bytes_empty, (LPVOID *)buffer, (DWORD *) bufsiz, NULL, &junk, 0 );
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
2003-07-11 03:15:28 +00:00
|
|
|
#endif
|
2004-03-05 02:41:43 +00:00
|
|
|
if ( result != DS_OK )
|
|
|
|
|
{
|
|
|
|
|
LOG->Warn( hr_ssprintf(result, "Couldn't lock the DirectSound buffer.") );
|
2002-12-21 07:21:49 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
write_cursor += num_bytes_empty;
|
2004-03-05 02:41:43 +00:00
|
|
|
if( write_cursor >= buffersize )
|
|
|
|
|
write_cursor -= buffersize;
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2003-01-31 21:34:15 +00:00
|
|
|
buffer_bytes_filled += num_bytes_empty;
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
/* Increment last_cursor_pos to point at where the data we're about to
|
|
|
|
|
* ask for will actually be played. */
|
2004-01-12 02:19:56 +00:00
|
|
|
last_cursor_pos += num_bytes_empty / bytes_per_frame();
|
2002-12-21 07:21:49 +00:00
|
|
|
buffer_locked = true;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2004-03-05 02:41:43 +00:00
|
|
|
void DSoundBuf::release_output_buf( char *buffer, unsigned bufsiz )
|
2002-12-21 07:21:49 +00:00
|
|
|
{
|
2004-03-05 02:41:43 +00:00
|
|
|
buf->Unlock( buffer, bufsiz, NULL, 0 );
|
2002-12-21 07:21:49 +00:00
|
|
|
buffer_locked = false;
|
|
|
|
|
}
|
|
|
|
|
|
2004-01-19 22:21:57 +00:00
|
|
|
int64_t DSoundBuf::GetPosition() const
|
2002-12-21 07:21:49 +00:00
|
|
|
{
|
|
|
|
|
DWORD cursor, junk;
|
2004-03-05 02:41:43 +00:00
|
|
|
buf->GetCurrentPosition( &cursor, &junk );
|
2004-01-12 02:19:56 +00:00
|
|
|
int cursor_frames = int(cursor) / bytes_per_frame();
|
|
|
|
|
int write_cursor_frames = write_cursor / bytes_per_frame();
|
2002-12-21 07:21:49 +00:00
|
|
|
|
2002-12-29 00:19:05 +00:00
|
|
|
int frames_behind = write_cursor_frames - cursor_frames;
|
2004-01-19 22:21:57 +00:00
|
|
|
/* frames_behind will be 0 if we're called before the buffer starts playing:
|
|
|
|
|
* both write_cursor_frames and cursor_frames will be 0. */
|
|
|
|
|
if( frames_behind < 0 )
|
2002-12-21 07:21:49 +00:00
|
|
|
frames_behind += buffersize_frames(); /* unwrap */
|
|
|
|
|
|
2004-01-19 22:21:57 +00:00
|
|
|
int64_t ret = last_cursor_pos - frames_behind;
|
2002-12-21 07:21:49 +00:00
|
|
|
|
|
|
|
|
/* Failsafe: never return a value smaller than we've already returned.
|
|
|
|
|
* This can happen once in a while in underrun conditions. */
|
2004-03-05 02:41:43 +00:00
|
|
|
ret = max( LastPosition, ret );
|
2002-12-21 07:21:49 +00:00
|
|
|
LastPosition = ret;
|
|
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DSoundBuf::Play()
|
|
|
|
|
{
|
2004-02-27 06:47:07 +00:00
|
|
|
if( playing )
|
|
|
|
|
return;
|
2004-03-05 02:41:43 +00:00
|
|
|
buf->Play( 0, 0, DSBPLAY_LOOPING );
|
2004-02-27 06:47:07 +00:00
|
|
|
playing = true;
|
2002-12-21 07:21:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void DSoundBuf::Stop()
|
|
|
|
|
{
|
2004-02-27 06:47:07 +00:00
|
|
|
if( !playing )
|
|
|
|
|
return;
|
|
|
|
|
|
2002-12-21 07:21:49 +00:00
|
|
|
buf->Stop();
|
|
|
|
|
buf->SetCurrentPosition(0);
|
2004-02-18 08:23:33 +00:00
|
|
|
|
2004-01-19 22:21:57 +00:00
|
|
|
last_cursor_pos = write_cursor = buffer_bytes_filled = 0;
|
2004-01-20 08:41:20 +00:00
|
|
|
LastPosition = 0;
|
2004-02-18 08:23:33 +00:00
|
|
|
|
|
|
|
|
/* When stopped and rewound, the play and write cursors should both be 0. */
|
2004-03-05 03:20:57 +00:00
|
|
|
/* This isn't true on some broken cards. */
|
|
|
|
|
// DWORD play, write;
|
|
|
|
|
// buf->GetCurrentPosition( &play, &write );
|
2004-06-16 00:38:31 +00:00
|
|
|
// ASSERT_M( play == 0 && write == 0, ssprintf("%i, %i", play, write) );
|
2004-02-27 06:47:07 +00:00
|
|
|
|
|
|
|
|
playing = false;
|
2002-12-13 08:33:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
2004-05-15 20:28:17 +00:00
|
|
|
* (c) 2002-2004 Glenn Maynard
|
|
|
|
|
* All rights reserved.
|
|
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
|
* copy of this software and associated documentation files (the
|
|
|
|
|
* "Software"), to deal in the Software without restriction, including
|
|
|
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
|
|
|
* distribute, and/or sell copies of the Software, and to permit persons to
|
|
|
|
|
* whom the Software is furnished to do so, provided that the above
|
|
|
|
|
* copyright notice(s) and this permission notice appear in all copies of
|
|
|
|
|
* the Software and that both the above copyright notice(s) and this
|
|
|
|
|
* permission notice appear in supporting documentation.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
|
|
|
* THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS
|
|
|
|
|
* INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT
|
|
|
|
|
* OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
|
|
|
* OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
|
|
|
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
|
|
|
* PERFORMANCE OF THIS SOFTWARE.
|
2002-12-13 08:33:25 +00:00
|
|
|
*/
|