mirror of https://github.com/snes9xgit/snes9x.git
420 lines
11 KiB
C++
420 lines
11 KiB
C++
/*****************************************************************************\
|
|
Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
|
|
This file is licensed under the Snes9x License.
|
|
For further information, consult the LICENSE file in the root directory.
|
|
\*****************************************************************************/
|
|
|
|
#include "CXAudio2.h"
|
|
#include "../snes9x.h"
|
|
#include "../apu/apu.h"
|
|
#include "wsnes9x.h"
|
|
#include <process.h>
|
|
#include "dxerr.h"
|
|
#include "commctrl.h"
|
|
#include <assert.h>
|
|
|
|
/* CXAudio2
|
|
Implements audio output through XAudio2.
|
|
Basic idea: one master voice and one source voice are created;
|
|
the source voice consumes buffers queued by PushBuffer, plays them through
|
|
the master voice and calls OnBufferEnd after each buffer.
|
|
ProcessSound copies new samples into the buffer and queues them for playback.
|
|
*/
|
|
|
|
/* Construction/Destruction
|
|
*/
|
|
CXAudio2::CXAudio2(void)
|
|
{
|
|
pXAudio2 = NULL;
|
|
pSourceVoice = NULL;
|
|
pMasterVoice = NULL;
|
|
|
|
sum_bufferSize = singleBufferBytes \
|
|
= singleBufferSamples = blockCount = 0;
|
|
soundBuffer = NULL;
|
|
initDone = false;
|
|
}
|
|
|
|
CXAudio2::~CXAudio2(void)
|
|
{
|
|
DeInitXAudio2();
|
|
}
|
|
|
|
INT_PTR CALLBACK DlgXAudio2InitError(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
|
|
{
|
|
switch (msg)
|
|
{
|
|
case WM_INITDIALOG:
|
|
{
|
|
// display warning icon and set text of sys control (too long for resource)
|
|
HICON aIcn = LoadIcon(NULL, IDI_WARNING);
|
|
SendDlgItemMessage(hDlg, IDC_STATIC_ICON, STM_SETICON, (WPARAM)aIcn, 0);
|
|
SetDlgItemText(hDlg, IDC_SYSLINK_DX, TEXT("Unable to initialize XAudio2.\
|
|
You will not be able to hear any sound effects or music while playing.\n\n\
|
|
This is usually caused by not having a recent DirectX9 runtime installed.\n\
|
|
You can download the most recent DirectX9 runtime here:\n\n\
|
|
<a href=\"https://www.microsoft.com/en-us/download/details.aspx?id=35\">https://www.microsoft.com/en-us/download/details.aspx?id=35</a>"));
|
|
|
|
// center dialog on parent
|
|
HWND parent = GetParent(hDlg);
|
|
RECT rcParent, rcSelf;
|
|
GetWindowRect(parent, &rcParent);
|
|
GetWindowRect(hDlg, &rcSelf);
|
|
|
|
SetWindowPos(hDlg,
|
|
HWND_TOP,
|
|
rcParent.left + ((rcParent.right - rcParent.left) - (rcSelf.right - rcSelf.left)) / 2,
|
|
rcParent.top + ((rcParent.bottom - rcParent.top) - (rcSelf.bottom - rcSelf.top)) / 2,
|
|
0, 0,
|
|
SWP_NOSIZE);
|
|
}
|
|
return true;
|
|
case WM_COMMAND:
|
|
switch (LOWORD(wParam))
|
|
{
|
|
case IDOK:
|
|
if (HIWORD(wParam) == BN_CLICKED) {
|
|
EndDialog(hDlg, IDOK);
|
|
return TRUE;
|
|
}
|
|
}
|
|
break;
|
|
case WM_NOTIFY:
|
|
switch (((LPNMHDR)lParam)->code)
|
|
{
|
|
case NM_CLICK: // Fall through to the next case.
|
|
case NM_RETURN:
|
|
{
|
|
PNMLINK pNMLink = (PNMLINK)lParam;
|
|
LITEM item = pNMLink->item;
|
|
|
|
// open link with registered application
|
|
ShellExecute(NULL, L"open", item.szUrl, NULL, NULL, SW_SHOW);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
/* CXAudio2::InitXAudio2
|
|
initializes the XAudio2 object
|
|
-----
|
|
returns true if successful, false otherwise
|
|
*/
|
|
bool CXAudio2::InitXAudio2(void)
|
|
{
|
|
if(initDone)
|
|
return true;
|
|
|
|
DeInitXAudio2();
|
|
|
|
HRESULT hr;
|
|
if ( FAILED(hr = XAudio2Create( &pXAudio2, 0 , XAUDIO2_DEFAULT_PROCESSOR ) ) ) {
|
|
DXTRACE_ERR_MSGBOX(TEXT("Unable to create XAudio2 object."),hr);
|
|
DialogBox(GUI.hInstance, MAKEINTRESOURCE(IDD_DIALOG_XAUDIO2_INIT_ERROR), GUI.hWnd, DlgXAudio2InitError);
|
|
return false;
|
|
}
|
|
initDone = true;
|
|
pXAudio2->RegisterForCallbacks(this);
|
|
return true;
|
|
}
|
|
|
|
/* CXAudio2::InitVoices
|
|
initializes the voice objects with the current audio settings
|
|
-----
|
|
returns true if successful, false otherwise
|
|
*/
|
|
bool CXAudio2::InitVoices(void)
|
|
{
|
|
HRESULT hr;
|
|
// subtract -1, we added "Default" as first index
|
|
int device_index = FindDeviceIndex(GUI.AudioDevice) - 1;
|
|
if (device_index < 0)
|
|
device_index = 0;
|
|
|
|
if ( FAILED(hr = pXAudio2->CreateMasteringVoice( &pMasterVoice, 2,
|
|
Settings.SoundPlaybackRate, 0, device_index, NULL ) ) ) {
|
|
DXTRACE_ERR_MSGBOX(TEXT("Unable to create mastering voice."),hr);
|
|
return false;
|
|
}
|
|
|
|
WAVEFORMATEX wfx;
|
|
wfx.wFormatTag = WAVE_FORMAT_PCM;
|
|
wfx.nChannels = 2;
|
|
wfx.nSamplesPerSec = Settings.SoundPlaybackRate;
|
|
wfx.nBlockAlign = 2 * 2;
|
|
wfx.wBitsPerSample = 16;
|
|
wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
|
|
wfx.cbSize = 0;
|
|
|
|
if( FAILED(hr = pXAudio2->CreateSourceVoice(&pSourceVoice, (WAVEFORMATEX*)&wfx,
|
|
XAUDIO2_VOICE_NOSRC, XAUDIO2_DEFAULT_FREQ_RATIO, this, NULL, NULL ) ) ) {
|
|
DXTRACE_ERR_MSGBOX(TEXT("Unable to create source voice."),hr);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* CXAudio2::DeInitXAudio2
|
|
deinitializes all objects
|
|
*/
|
|
void CXAudio2::DeInitXAudio2(void)
|
|
{
|
|
initDone = false;
|
|
DeInitVoices();
|
|
if(pXAudio2) {
|
|
pXAudio2->Release();
|
|
pXAudio2 = NULL;
|
|
}
|
|
}
|
|
|
|
/* CXAudio2::DeInitVoices
|
|
deinitializes the voice objects and buffers
|
|
*/
|
|
void CXAudio2::DeInitVoices(void)
|
|
{
|
|
if(pSourceVoice) {
|
|
StopPlayback();
|
|
pSourceVoice->DestroyVoice();
|
|
pSourceVoice = NULL;
|
|
}
|
|
if(pMasterVoice) {
|
|
pMasterVoice->DestroyVoice();
|
|
pMasterVoice = NULL;
|
|
}
|
|
if(soundBuffer) {
|
|
delete [] soundBuffer;
|
|
soundBuffer = NULL;
|
|
}
|
|
}
|
|
|
|
/* CXAudio2::OnBufferEnd
|
|
callback function called by the source voice
|
|
IN:
|
|
pBufferContext - unused
|
|
*/
|
|
void CXAudio2::OnBufferEnd(void *pBufferContext)
|
|
{
|
|
InterlockedDecrement(&bufferCount);
|
|
SetEvent(GUI.SoundSyncEvent);
|
|
}
|
|
|
|
/* CXAudio2::PushBuffer
|
|
pushes one buffer onto the source voice playback queue
|
|
IN:
|
|
AudioBytes - size of the buffer
|
|
pAudioData - pointer to the buffer
|
|
pContext - context passed to the callback, currently unused
|
|
*/
|
|
void CXAudio2::PushBuffer(UINT32 AudioBytes,BYTE *pAudioData,void *pContext)
|
|
{
|
|
XAUDIO2_BUFFER xa2buffer={0};
|
|
xa2buffer.AudioBytes=AudioBytes;
|
|
xa2buffer.pAudioData=pAudioData;
|
|
xa2buffer.pContext=pContext;
|
|
InterlockedIncrement(&bufferCount);
|
|
pSourceVoice->SubmitSourceBuffer(&xa2buffer);
|
|
}
|
|
|
|
/* CXAudio2::SetupSound
|
|
applies current sound settings by recreating the voice objects and buffers
|
|
-----
|
|
returns true if successful, false otherwise
|
|
*/
|
|
bool CXAudio2::SetupSound()
|
|
{
|
|
if(!initDone)
|
|
return false;
|
|
|
|
DeInitVoices();
|
|
|
|
blockCount = 8;
|
|
UINT32 blockTime = GUI.SoundBufferSize / blockCount;
|
|
|
|
singleBufferSamples = (Settings.SoundPlaybackRate * blockTime) / 1000;
|
|
singleBufferSamples *= 2;
|
|
singleBufferBytes = singleBufferSamples * 2;
|
|
sum_bufferSize = singleBufferBytes * blockCount;
|
|
|
|
if (InitVoices())
|
|
{
|
|
soundBuffer = new uint8[sum_bufferSize];
|
|
writeOffset = 0;
|
|
partialOffset = 0;
|
|
}
|
|
else {
|
|
DeInitVoices();
|
|
return false;
|
|
}
|
|
|
|
bufferCount = 0;
|
|
|
|
BeginPlayback();
|
|
|
|
return true;
|
|
}
|
|
|
|
void CXAudio2::SetVolume(double volume)
|
|
{
|
|
pSourceVoice->SetVolume(volume);
|
|
}
|
|
|
|
void CXAudio2::BeginPlayback()
|
|
{
|
|
pSourceVoice->Start(0);
|
|
}
|
|
|
|
void CXAudio2::StopPlayback()
|
|
{
|
|
pSourceVoice->Stop(0);
|
|
}
|
|
|
|
int CXAudio2::GetAvailableBytes()
|
|
{
|
|
return ((blockCount - bufferCount) * singleBufferBytes) - partialOffset;
|
|
}
|
|
|
|
/* CXAudio2::ProcessSound
|
|
The mixing function called by the sound core when new samples are available.
|
|
SoundBuffer is divided into blockCount blocks. If there are enough available samples and a free block,
|
|
the block is filled and queued to the source voice. bufferCount is increased by pushbuffer and decreased by
|
|
the OnBufferComplete callback.
|
|
*/
|
|
void CXAudio2::ProcessSound()
|
|
{
|
|
int freeBytes = GetAvailableBytes();
|
|
|
|
if (Settings.DynamicRateControl)
|
|
{
|
|
S9xUpdateDynamicRate(freeBytes, sum_bufferSize);
|
|
}
|
|
|
|
UINT32 availableSamples;
|
|
|
|
availableSamples = S9xGetSampleCount();
|
|
|
|
if (Settings.DynamicRateControl && !Settings.SoundSync)
|
|
{
|
|
// Using rate control, we should always keep the emulator's sound buffers empty to
|
|
// maintain an accurate measurement.
|
|
if (availableSamples > (freeBytes >> 1))
|
|
{
|
|
S9xClearSamples();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(!initDone)
|
|
return;
|
|
|
|
if(Settings.SoundSync && !Settings.TurboMode && !Settings.Mute)
|
|
{
|
|
// no sound sync when speed is not set to 100%
|
|
while((freeBytes >> 1) < availableSamples)
|
|
{
|
|
ResetEvent(GUI.SoundSyncEvent);
|
|
if(!GUI.AllowSoundSync || WaitForSingleObject(GUI.SoundSyncEvent, 1000) != WAIT_OBJECT_0)
|
|
{
|
|
S9xClearSamples();
|
|
return;
|
|
}
|
|
freeBytes = GetAvailableBytes();
|
|
}
|
|
}
|
|
|
|
if (partialOffset != 0) {
|
|
assert(partialOffset < singleBufferBytes);
|
|
assert(bufferCount < blockCount);
|
|
BYTE *offsetBuffer = soundBuffer + writeOffset + partialOffset;
|
|
UINT32 samplesleftinblock = (singleBufferBytes - partialOffset) >> 1;
|
|
|
|
if (availableSamples < samplesleftinblock)
|
|
{
|
|
S9xMixSamples(offsetBuffer, availableSamples);
|
|
partialOffset += availableSamples << 1;
|
|
assert(partialOffset < singleBufferBytes);
|
|
availableSamples = 0;
|
|
}
|
|
else
|
|
{
|
|
S9xMixSamples(offsetBuffer, samplesleftinblock);
|
|
partialOffset = 0;
|
|
availableSamples -= samplesleftinblock;
|
|
PushBuffer(singleBufferBytes, soundBuffer + writeOffset, NULL);
|
|
writeOffset += singleBufferBytes;
|
|
writeOffset %= sum_bufferSize;
|
|
}
|
|
}
|
|
|
|
while (availableSamples >= singleBufferSamples && bufferCount < blockCount) {
|
|
BYTE *curBuffer = soundBuffer + writeOffset;
|
|
S9xMixSamples(curBuffer, singleBufferSamples);
|
|
PushBuffer(singleBufferBytes, curBuffer, NULL);
|
|
writeOffset += singleBufferBytes;
|
|
writeOffset %= sum_bufferSize;
|
|
availableSamples -= singleBufferSamples;
|
|
}
|
|
|
|
// need to check this is less than a single buffer, otherwise we have a race condition with bufferCount
|
|
if (availableSamples > 0 && availableSamples < singleBufferSamples && bufferCount < blockCount) {
|
|
S9xMixSamples(soundBuffer + writeOffset, availableSamples);
|
|
partialOffset = availableSamples << 1;
|
|
assert(partialOffset < singleBufferBytes);
|
|
}
|
|
}
|
|
|
|
/* CXAudio2::GetDeviceList
|
|
get a list of the available output devices
|
|
-----
|
|
returns a vector of display names
|
|
*/
|
|
std::vector<std::wstring> CXAudio2::GetDeviceList()
|
|
{
|
|
std::vector<std::wstring> device_list;
|
|
|
|
if (pXAudio2)
|
|
{
|
|
UINT32 num_devices;
|
|
pXAudio2->GetDeviceCount(&num_devices);
|
|
|
|
device_list.push_back(_T("Default"));
|
|
|
|
for (unsigned int i = 0; i < num_devices; i++)
|
|
{
|
|
XAUDIO2_DEVICE_DETAILS device_details;
|
|
if (SUCCEEDED(pXAudio2->GetDeviceDetails(i, &device_details)))
|
|
{
|
|
device_list.push_back(device_details.DisplayName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return device_list;
|
|
}
|
|
|
|
/* CXAudio2::FindDeviceIndex
|
|
find a device name in the list of possible output devices
|
|
-----
|
|
returns the index in the device list returned by GetDeviceList
|
|
*/
|
|
int CXAudio2::FindDeviceIndex(TCHAR *audio_device)
|
|
{
|
|
std::vector<std::wstring> device_list = GetDeviceList();
|
|
|
|
int index = 0;
|
|
|
|
for (int i = 0; i < device_list.size(); i++)
|
|
{
|
|
if (_tcsstr(device_list[i].c_str(), audio_device) != NULL)
|
|
{
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return index;
|
|
}
|