diff --git a/src/wx/faudio.cpp b/src/wx/faudio.cpp new file mode 100644 index 00000000..b7b53066 --- /dev/null +++ b/src/wx/faudio.cpp @@ -0,0 +1,625 @@ +#ifndef NO_XAUDIO2 + +// Application +#include "wxvbam.h" +#include + +// Interface +#include "../common/ConfigManager.h" +#include "../common/SoundDriver.h" + +// XAudio2 +#include +#endif + +// MMDevice API +#include +#include +#include + +// Internals +#include "../System.h" // for systemMessage() +#include "../gba/Globals.h" + +int GetXA2Devices(IXAudio2* xa, wxArrayString* names, wxArrayString* ids, + const wxString* match) +{ + HRESULT hr; + UINT32 dev_count = 0; + hr = xa->FAudio_GetDeviceCount(&dev_count); + + if (hr != S_OK) { + wxLogError(_("XAudio2: Enumerating devices failed!")); + return true; + } else { + XAUDIO2_DEVICE_DETAILS dd; + + for (UINT32 i = 0; i < dev_count; i++) { + hr = xa->GetDeviceDetails(i, &dd); + + if (hr != S_OK) { + continue; + } else { + if (ids) { + ids->push_back(dd.DeviceID); + names->push_back(dd.DisplayName); + } else if (*match == dd.DeviceID) + return i; + } + } + } + + return -1; +} + +bool GetXA2Devices(wxArrayString& names, wxArrayString& ids) +{ + HRESULT hr; + IXAudio2* xa = NULL; + UINT32 flags = 0; +#ifdef _DEBUG + flags = FAUDIO_DEBUG_ENGINE; +#endif + hr = XAudio2Create(&xa, flags); + + if (hr != S_OK) { + wxLogError(_("The XAudio2 interface failed to initialize!")); + return false; + } + + GetXA2Devices(xa, &names, &ids, NULL); + xa->Release(); + return true; +} + +static int XA2GetDev(IXAudio2* xa) +{ + if (gopts.audio_dev.empty()) + return 0; + else { + int ret = GetXA2Devices(xa, NULL, NULL, &gopts.audio_dev); + return ret < 0 ? 0 : ret; + } +} + +class XAudio2_Output; + +static void xaudio2_device_changed(XAudio2_Output*); + +class XAudio2_Device_Notifier : public IMMNotificationClient { + volatile LONG registered; + IMMDeviceEnumerator* pEnumerator; + + std::wstring last_device; + + CRITICAL_SECTION lock; + std::vector instances; + +public: + XAudio2_Device_Notifier() + : registered(0) + { + InitializeCriticalSection(&lock); + } + ~XAudio2_Device_Notifier() + { + DeleteCriticalSection(&lock); + } + + ULONG STDMETHODCALLTYPE AddRef() + { + return 1; + } + + ULONG STDMETHODCALLTYPE Release() + { + return 1; + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID** ppvInterface) + { + if (IID_IUnknown == riid) { + *ppvInterface = (IUnknown*)this; + } else if (__uuidof(IMMNotificationClient) == riid) { + *ppvInterface = (IMMNotificationClient*)this; + } else { + *ppvInterface = NULL; + return E_NOINTERFACE; + } + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) + { + if (flow == eRender && last_device.compare(pwstrDeviceId) != 0) { + last_device = pwstrDeviceId; + EnterCriticalSection(&lock); + + for (auto it = instances.begin(); it < instances.end(); ++it) { + xaudio2_device_changed(*it); + } + + LeaveCriticalSection(&lock); + } + + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { return S_OK; } + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { return S_OK; } + + void do_register(XAudio2_Output* p_instance) + { + if (InterlockedIncrement(®istered) == 1) { + pEnumerator = NULL; + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator); + + if (SUCCEEDED(hr)) { + pEnumerator->RegisterEndpointNotificationCallback(this); + } + } + + EnterCriticalSection(&lock); + instances.push_back(p_instance); + LeaveCriticalSection(&lock); + } + + void do_unregister(XAudio2_Output* p_instance) + { + if (InterlockedDecrement(®istered) == 0) { + if (pEnumerator) { + pEnumerator->UnregisterEndpointNotificationCallback(this); + pEnumerator->Release(); + pEnumerator = NULL; + } + } + + EnterCriticalSection(&lock); + + for (auto it = instances.begin(); it < instances.end(); ++it) { + if (*it == p_instance) { + instances.erase(it); + break; + } + } + + LeaveCriticalSection(&lock); + } +} g_notifier; + +// Synchronization Event +class XAudio2_BufferNotify : public IXAudio2VoiceCallback { +public: + HANDLE hBufferEndEvent; + + XAudio2_BufferNotify() + { + hBufferEndEvent = NULL; + hBufferEndEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + assert(hBufferEndEvent != NULL); + } + + ~XAudio2_BufferNotify() + { + CloseHandle(hBufferEndEvent); + hBufferEndEvent = NULL; + } + + STDMETHOD_(void, OnBufferEnd) + (void* pBufferContext) + { + assert(hBufferEndEvent != NULL); + SetEvent(hBufferEndEvent); + } + + // dummies: + STDMETHOD_(void, OnVoiceProcessingPassStart) + (UINT32 BytesRequired) {} + STDMETHOD_(void, OnVoiceProcessingPassEnd) + () {} + STDMETHOD_(void, OnStreamEnd) + () {} + STDMETHOD_(void, OnBufferStart) + (void* pBufferContext) {} + STDMETHOD_(void, OnLoopEnd) + (void* pBufferContext) {} + STDMETHOD_(void, OnVoiceError) + (void* pBufferContext, HRESULT Error){}; +}; + +// Class Declaration +class XAudio2_Output + : public SoundDriver { +public: + XAudio2_Output(); + ~XAudio2_Output(); + + // Initialization + bool init(long sampleRate); + + // Sound Data Feed + void write(uint16_t* finalWave, int length); + + // Play Control + void pause(); + void resume(); + void reset(); + void close(); + void device_change(); + + // Configuration Changes + void setThrottle(unsigned short throttle); + +private: + bool failed; + bool initialized; + bool playing; + UINT32 freq; + UINT32 bufferCount; + BYTE* buffers; + int currentBuffer; + int soundBufferLen; + + volatile bool device_changed; + + IXAudio2* xaud; + IXAudio2MasteringVoice* mVoice; // listener + IXAudio2SourceVoice* sVoice; // sound source + XAUDIO2_BUFFER buf; + XAUDIO2_VOICE_STATE vState; + XAudio2_BufferNotify notify; // buffer end notification +}; + +// Class Implementation +XAudio2_Output::XAudio2_Output() +{ + failed = false; + initialized = false; + playing = false; + freq = 0; + bufferCount = gopts.audio_buffers; + buffers = NULL; + currentBuffer = 0; + device_changed = false; + xaud = NULL; + mVoice = NULL; + sVoice = NULL; + ZeroMemory(&buf, sizeof(buf)); + ZeroMemory(&vState, sizeof(vState)); + g_notifier.do_register(this); +} + +XAudio2_Output::~XAudio2_Output() +{ + g_notifier.do_unregister(this); + close(); +} + +void XAudio2_Output::close() +{ + initialized = false; + + if (sVoice) { + if (playing) { + HRESULT hr = sVoice->Stop(0); + assert(hr == S_OK); + } + + sVoice->DestroyVoice(); + sVoice = NULL; + } + + if (buffers) { + free(buffers); + buffers = NULL; + } + + if (mVoice) { + mVoice->DestroyVoice(); + mVoice = NULL; + } + + if (xaud) { + xaud->Release(); + xaud = NULL; + } +} + +void XAudio2_Output::device_change() +{ + device_changed = true; +} + +bool XAudio2_Output::init(long sampleRate) +{ + if (failed || initialized) + return false; + + HRESULT hr; + // Initialize XAudio2 + UINT32 flags = 0; + //#ifdef _DEBUG + // flags = XAUDIO2_DEBUG_ENGINE; + //#endif + hr = XAudio2Create(&xaud, flags); + + if (hr != S_OK) { + wxLogError(_("The XAudio2 interface failed to initialize!")); + failed = true; + return false; + } + + freq = sampleRate; + // calculate the number of samples per frame first + // then multiply it with the size of a sample frame (16 bit * stereo) + soundBufferLen = (freq / 60) * 4; + // create own buffers to store sound data because it must not be + // manipulated while the voice plays from it + buffers = (BYTE*)malloc((bufferCount + 1) * soundBufferLen); + // + 1 because we need one temporary buffer when all others are in use + WAVEFORMATEX wfx; + ZeroMemory(&wfx, sizeof(wfx)); + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.nChannels = 2; + wfx.nSamplesPerSec = freq; + wfx.wBitsPerSample = 16; + wfx.nBlockAlign = wfx.nChannels * (wfx.wBitsPerSample / 8); + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + // create sound receiver + hr = xaud->CreateMasteringVoice( + &mVoice, + XAUDIO2_DEFAULT_CHANNELS, + XAUDIO2_DEFAULT_SAMPLERATE, + 0, + XA2GetDev(xaud), + NULL); + + if (hr != S_OK) { + wxLogError(_("XAudio2: Creating mastering voice failed!")); + failed = true; + return false; + } + + // create sound emitter + hr = xaud->CreateSourceVoice(&sVoice, &wfx, 0, 4.0f, ¬ify); + + if (hr != S_OK) { + wxLogError(_("XAudio2: Creating source voice failed!")); + failed = true; + return false; + } + + if (gopts.upmix) { + // set up stereo upmixing + XAUDIO2_DEVICE_DETAILS dd; + ZeroMemory(&dd, sizeof(dd)); + hr = xaud->FAudio_GetDeviceDetails(0, &dd); + assert(hr == S_OK); + float* matrix = NULL; + matrix = (float*)malloc(sizeof(float) * 2 * dd.OutputFormat.Format.nChannels); + + if (matrix == NULL) + return false; + + bool matrixAvailable = true; + + switch (dd.OutputFormat.Format.nChannels) { + case 4: // 4.0 + //Speaker \ Left Source Right Source + /*Front L*/ matrix[0] = 1.0000f; + matrix[1] = 0.0000f; + /*Front R*/ matrix[2] = 0.0000f; + matrix[3] = 1.0000f; + /*Back L*/ matrix[4] = 1.0000f; + matrix[5] = 0.0000f; + /*Back R*/ matrix[6] = 0.0000f; + matrix[7] = 1.0000f; + break; + + case 5: // 5.0 + //Speaker \ Left Source Right Source + /*Front L*/ matrix[0] = 1.0000f; + matrix[1] = 0.0000f; + /*Front R*/ matrix[2] = 0.0000f; + matrix[3] = 1.0000f; + /*Front C*/ matrix[4] = 0.7071f; + matrix[5] = 0.7071f; + /*Side L*/ matrix[6] = 1.0000f; + matrix[7] = 0.0000f; + /*Side R*/ matrix[8] = 0.0000f; + matrix[9] = 1.0000f; + break; + + case 6: // 5.1 + //Speaker \ Left Source Right Source + /*Front L*/ matrix[0] = 1.0000f; + matrix[1] = 0.0000f; + /*Front R*/ matrix[2] = 0.0000f; + matrix[3] = 1.0000f; + /*Front C*/ matrix[4] = 0.7071f; + matrix[5] = 0.7071f; + /*LFE */ matrix[6] = 0.0000f; + matrix[7] = 0.0000f; + /*Side L*/ matrix[8] = 1.0000f; + matrix[9] = 0.0000f; + /*Side R*/ matrix[10] = 0.0000f; + matrix[11] = 1.0000f; + break; + + case 7: // 6.1 + //Speaker \ Left Source Right Source + /*Front L*/ matrix[0] = 1.0000f; + matrix[1] = 0.0000f; + /*Front R*/ matrix[2] = 0.0000f; + matrix[3] = 1.0000f; + /*Front C*/ matrix[4] = 0.7071f; + matrix[5] = 0.7071f; + /*LFE */ matrix[6] = 0.0000f; + matrix[7] = 0.0000f; + /*Side L*/ matrix[8] = 1.0000f; + matrix[9] = 0.0000f; + /*Side R*/ matrix[10] = 0.0000f; + matrix[11] = 1.0000f; + /*Back C*/ matrix[12] = 0.7071f; + matrix[13] = 0.7071f; + break; + + case 8: // 7.1 + //Speaker \ Left Source Right Source + /*Front L*/ matrix[0] = 1.0000f; + matrix[1] = 0.0000f; + /*Front R*/ matrix[2] = 0.0000f; + matrix[3] = 1.0000f; + /*Front C*/ matrix[4] = 0.7071f; + matrix[5] = 0.7071f; + /*LFE */ matrix[6] = 0.0000f; + matrix[7] = 0.0000f; + /*Back L*/ matrix[8] = 1.0000f; + matrix[9] = 0.0000f; + /*Back R*/ matrix[10] = 0.0000f; + matrix[11] = 1.0000f; + /*Side L*/ matrix[12] = 1.0000f; + matrix[13] = 0.0000f; + /*Side R*/ matrix[14] = 0.0000f; + matrix[15] = 1.0000f; + break; + + default: + matrixAvailable = false; + break; + } + + if (matrixAvailable) { + hr = sVoice->SetOutputMatrix(NULL, 2, dd.OutputFormat.Format.nChannels, matrix); + assert(hr == S_OK); + } + + free(matrix); + matrix = NULL; + } + + hr = sVoice->Start(0); + assert(hr == S_OK); + playing = true; + currentBuffer = 0; + device_changed = false; + initialized = true; + return true; +} + +void XAudio2_Output::write(uint16_t* finalWave, int length) +{ + if (!initialized || failed) + return; + + while (true) { + if (device_changed) { + close(); + + if (!init(freq)) + return; + } + + sVoice->GetState(&vState); + assert(vState.BuffersQueued <= bufferCount); + + if (vState.BuffersQueued < bufferCount) { + if (vState.BuffersQueued == 0) { + // buffers ran dry + if (systemVerbose & VERBOSE_SOUNDOUTPUT) { + static unsigned int i = 0; + log("XAudio2: Buffers were not refilled fast enough (i=%i)\n", i++); + } + } + + // there is at least one free buffer + break; + } else { + // the maximum number of buffers is currently queued + if (!speedup && throttle && !gba_joybus_active) { + // wait for one buffer to finish playing + if (WaitForSingleObject(notify.hBufferEndEvent, 10000) == WAIT_TIMEOUT) { + device_changed = true; + } + } else { + // drop current audio frame + return; + } + } + } + + // copy & protect the audio data in own memory area while playing it + CopyMemory(&buffers[currentBuffer * soundBufferLen], finalWave, soundBufferLen); + buf.AudioBytes = soundBufferLen; + buf.pAudioData = &buffers[currentBuffer * soundBufferLen]; + currentBuffer++; + currentBuffer %= (bufferCount + 1); // + 1 because we need one temporary buffer + HRESULT hr = sVoice->SubmitSourceBuffer(&buf); // send buffer to queue + assert(hr == S_OK); +} + +void XAudio2_Output::pause() +{ + if (!initialized || failed) + return; + + if (playing) { + HRESULT hr = sVoice->Stop(0); + assert(hr == S_OK); + playing = false; + } +} + +void XAudio2_Output::resume() +{ + if (!initialized || failed) + return; + + if (!playing) { + HRESULT hr = sVoice->Start(0); + assert(hr == S_OK); + playing = true; + } +} + +void XAudio2_Output::reset() +{ + if (!initialized || failed) + return; + + if (playing) { + HRESULT hr = sVoice->Stop(0); + assert(hr == S_OK); + } + + sVoice->FlushSourceBuffers(); + sVoice->Start(0); + playing = true; +} + +void XAudio2_Output::setThrottle(unsigned short throttle_) +{ + if (!initialized || failed) + return; + + if (throttle_ == 0) + throttle_ = 100; + + HRESULT hr = sVoice->SetFrequencyRatio((float)throttle_ / 100.0f); + assert(hr == S_OK); +} + +void xaudio2_device_changed(XAudio2_Output* instance) +{ + instance->device_change(); +} + +SoundDriver* newXAudio2_Output() +{ + return new XAudio2_Output(); +} + +#endif // #ifndef NO_XAUDIO2