daedalus/Source/HLEAudio/WASAPISoundDriver.cpp
2021-12-11 12:55:43 +11:00

369 lines
12 KiB
C++

/****************************************************************************
* *
* Azimer's HLE Audio Plugin for Project64 Compatible N64 Emulators *
* http://www.apollo64.com/ *
* Copyright (C) 2000-2019 Azimer. All rights reserved. *
* *
* License: *
* GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html *
* *
****************************************************************************/
#include "common.h"
#if defined(ENABLE_BACKEND_WASAPI)
#include "WASAPISoundDriver.h"
#include "AudioSpec.h"
#include <stdio.h>
#include "SoundDriverFactory.h"
#include <audioclient.h>
#include <mmdeviceapi.h>
bool WASAPISoundDriver::ClassRegistered = WASAPISoundDriver::ValidateDriver() ?
SoundDriverFactory::RegisterSoundDriver(SND_DRIVER_WASAPI, WASAPISoundDriver::CreateSoundDriver, "WASAPI Driver (experimental)", 0) :
false;
// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC 10000000
#define REFTIMES_PER_MILLISEC 10000
#define EXIT_ON_ERROR(hres) \
if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk) \
if ((punk) != NULL) \
{ (punk)->Release(); (punk) = NULL; }
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
bool WASAPISoundDriver::ValidateDriver()
{
bool retVal = false;
/* Validate a windows audio services end point enumerator object will initialize */
CoInitializeEx(NULL, COINIT_MULTITHREADED);
const GUID CLSID_MMDeviceEnumerator_Test = { 0xBCDE0395, 0xE52F, 0x467C, 0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E };
const GUID IID_IMMDeviceEnumerator_Test = { 0xA95664D2, 0x9614, 0x4F35, 0xA7, 0x46, 0xDE, 0x8D, 0xB6, 0x36, 0x17, 0xE6 };
IUnknown* obj;
HRESULT hr = CoCreateInstance(CLSID_MMDeviceEnumerator_Test,
NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator_Test, (void**)&obj);
if (SUCCEEDED(hr))
{
obj->Release();
retVal = true;
}
CoUninitialize();
return retVal;
}
WASAPISoundDriver::WASAPISoundDriver()
{
bInitialized = false;
m_CoUninit = false;
hAudioThread = NULL;
}
WASAPISoundDriver::~WASAPISoundDriver()
{
DeInitialize();
}
BOOL WASAPISoundDriver::Initialize()
{
IMMDeviceEnumerator *testEnumerator = NULL;
HRESULT hr;
m_CoUninit = false;
hr = CoInitializeEx(0, COINIT_MULTITHREADED);
if (FAILED(hr) && (hr != RPC_E_CHANGED_MODE))
{
EXIT_ON_ERROR(hr);
}
else
{
m_CoUninit = true;
}
hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&testEnumerator);
EXIT_ON_ERROR(hr);
bInitialized = true;
return TRUE;
Exit:
SAFE_RELEASE(testEnumerator);
if (m_CoUninit == true)
CoUninitialize();
return FALSE;
}
void WASAPISoundDriver::DeInitialize()
{
DWORD exitCode;
if (bInitialized == true)
{
bStopAudioThread = true;
if (hAudioThread != NULL)
{
WaitForSingleObject(hAudioThread, 5000);
GetExitCodeThread(hAudioThread, &exitCode);
if (exitCode == STILL_ACTIVE)
{
TerminateThread(hAudioThread, (DWORD)-1);
}
hAudioThread = NULL;
}
}
if (m_CoUninit == true)
CoUninitialize();
}
void WASAPISoundDriver::SetFrequency(u32 Frequency)
{
UNREFERENCED_PARAMETER(Frequency);
if (hAudioThread == NULL)
{
bStopAudioThread = false;
hAudioThread = CreateThread(NULL, 0, AudioThreadProc, this, 0, NULL);
assert(hAudioThread != NULL);
}
}
void WASAPISoundDriver::StopAudio()
{
}
void WASAPISoundDriver::StartAudio()
{
}
void WASAPISoundDriver::SetVolume(u32 volume)
{
m_Volume = (1.0f - ((float)volume / 100.0f));
}
DWORD WINAPI WASAPISoundDriver::AudioThreadProc(LPVOID lpParameter)
{
WASAPISoundDriver* driver = (WASAPISoundDriver*)lpParameter;
REFERENCE_TIME hnsRequestedDuration = REFTIMES_PER_MILLISEC * (1000 / Configuration::getBackendFPS()) * Configuration::getBufferLevel();
REFERENCE_TIME hnsActualDuration;
HRESULT hr;
IMMDeviceEnumerator *pEnumerator = NULL;
IMMDevice *pDevice = NULL;
IAudioClient *pAudioClient = NULL;
IAudioRenderClient *pRenderClient = NULL;
WAVEFORMATEXTENSIBLE *pwfx = NULL;
UINT32 bufferFrameCount;
UINT32 numFramesAvailable;
UINT32 numFramesPadding;
BYTE *pData;
DWORD flags = 0;
WAVEFORMATEXTENSIBLE AudioFormat = {};
hr = CoInitializeEx(0, COINIT_MULTITHREADED);
EXIT_ON_ERROR(hr);
// Get ourselves a device enumerator. This can be used to determine which device we want to write to
hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
EXIT_ON_ERROR(hr);
hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice);
EXIT_ON_ERROR(hr);
hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&pAudioClient); EXIT_ON_ERROR(hr);
hr = pAudioClient->GetMixFormat((WAVEFORMATEX **)&pwfx); EXIT_ON_ERROR(hr);
AudioFormat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
AudioFormat.Format.nChannels = 2;
AudioFormat.Format.nSamplesPerSec = pwfx->Format.nSamplesPerSec;
AudioFormat.Format.wBitsPerSample = 32;
AudioFormat.Format.nBlockAlign = (AudioFormat.Format.wBitsPerSample / 8) * AudioFormat.Format.nChannels;
AudioFormat.Format.nAvgBytesPerSec = AudioFormat.Format.nSamplesPerSec * AudioFormat.Format.nBlockAlign;
AudioFormat.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
AudioFormat.dwChannelMask = 0x3;
AudioFormat.Samples.wValidBitsPerSample = 32;
AudioFormat.Samples.wSamplesPerBlock = 32;
AudioFormat.Samples.wReserved = 32;
AudioFormat.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
0,//AUDCLNT_STREAMFLAGS_RATEADJUST | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM,
hnsRequestedDuration,
0,
(WAVEFORMATEX *)&AudioFormat, //&AudioFormat,
NULL);
EXIT_ON_ERROR(hr)
REFERENCE_TIME pDefault;
REFERENCE_TIME pMinimum;
pAudioClient->GetDevicePeriod(&pDefault, &pMinimum);
// Get the actual size of the allocated buffer.
hr = pAudioClient->GetBufferSize(&bufferFrameCount); EXIT_ON_ERROR(hr);
hr = pAudioClient->GetService(IID_IAudioRenderClient,(void**)&pRenderClient); EXIT_ON_ERROR(hr);
// Grab the entire buffer for the initial fill operation.
hr = pRenderClient->GetBuffer(bufferFrameCount, &pData); EXIT_ON_ERROR(hr);
float ratio;
ratio = (float)driver->m_SamplesPerSecond / (float)AudioFormat.Format.nSamplesPerSec;
// Load the initial data into the shared buffer.
driver->LoadAiBufferResample(pData, bufferFrameCount, ratio);
hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags); EXIT_ON_ERROR(hr);
// Calculate the actual duration of the allocated buffer.
hnsActualDuration = ((REFTIMES_PER_SEC * bufferFrameCount) / AudioFormat.Format.nSamplesPerSec);
DEBUG_OUTPUT("Requested Duration: %i ms\n", (1000 / Configuration::getBackendFPS()));
DEBUG_OUTPUT("Actual Duration: %i ms\n", (int)(hnsActualDuration / REFTIMES_PER_MILLISEC));
DEBUG_OUTPUT("Buffer Frame Count: %i\n", bufferFrameCount);
// Let's play some shit...
hr = pAudioClient->Start(); EXIT_ON_ERROR(hr); // Start playing.
SetThreadPriority(driver->hAudioThread, THREAD_PRIORITY_TIME_CRITICAL);
// Each loop fills about half of the shared buffer.
while (driver->bStopAudioThread == false)
{
// See how much buffer space is available.
hr = pAudioClient->GetCurrentPadding(&numFramesPadding); EXIT_ON_ERROR(hr);
numFramesAvailable = bufferFrameCount - numFramesPadding;
if (numFramesAvailable > 0)
{
if (numFramesPadding == 0) DEBUG_OUTPUT("!");
// Grab all the available space in the shared buffer.
hr = pRenderClient->GetBuffer(numFramesAvailable, &pData); EXIT_ON_ERROR(hr);
ratio = (float)driver->m_SamplesPerSecond / (float)AudioFormat.Format.nSamplesPerSec;
driver->LoadAiBufferResample(pData, numFramesAvailable, ratio);
hr = pRenderClient->ReleaseBuffer(numFramesAvailable, flags); EXIT_ON_ERROR(hr);
hnsActualDuration = ((REFTIMES_PER_SEC * numFramesAvailable) / AudioFormat.Format.nSamplesPerSec);
}
else
{
Sleep((DWORD)(hnsActualDuration / REFTIMES_PER_MILLISEC / 2));
hnsActualDuration = REFTIMES_PER_MILLISEC * 2; // At least 1ms sleep
}
}
SetThreadPriority(driver->hAudioThread, THREAD_PRIORITY_NORMAL);
DEBUG_OUTPUT("WASAPI: Ending loop\n");
Sleep(10);
hr = pAudioClient->Stop(); EXIT_ON_ERROR(hr); // Stop playing.
// Graceful exit
goto NiceExit;
Exit:
DEBUG_OUTPUT("WASAPI: Error on thread\n");
MessageBox(NULL, "An error occurred in the WASAPI audio thread. This will result in no audio or a hang if prevent buffer overflow is checked. As this is an experimental audio code, please submit a bug report to github. Thank you!",
"AziAudio ERROR", MB_OK);
NiceExit:
CoTaskMemFree(pwfx);
SAFE_RELEASE(pEnumerator);
SAFE_RELEASE(pDevice);
SAFE_RELEASE(pAudioClient);
SAFE_RELEASE(pRenderClient);
CoUninitialize();
DEBUG_OUTPUT("WASAPI: Exiting thread\n");
return 0;
}
// TODO: Same as LoadAiBuffer but with a rudementary resample method added
u32 WASAPISoundDriver::LoadAiBufferResample(u8 *start, u32 frames, float ratio)
{
u32 samplesToMove = frames;// &0xFFFFFFFC;
u32 bytesToMove = (u32)(samplesToMove * ratio) * 4;
float *outp = (float *)(start);
s16 *inp = (s16 *)(m_Buffer + m_CurrentReadLoc);
float gain = m_Volume * 1.4f;
float Cinp0, Cinp1;
static float Linp0 = 0.0f, Linp1 = 0.0f;
static float accum = 0.0f;
if (start == NULL) return 0;
assert(bytesToMove <= m_MaxBufferSize); // We shouldn't be asking for more.
m_DMAEnabled = (*AudioInfo.AI_CONTROL_REG & AI_CONTROL_DMA_ON) == AI_CONTROL_DMA_ON;
if ((bytesToMove > m_MaxBufferSize) || (m_DMAEnabled == false))
{
while (samplesToMove > 0)
{
outp[0] = Linp0; outp[1] = Linp1;
outp += 2;
samplesToMove -= 1;
}
return frames;
}
#ifdef _WIN32
WaitForSingleObject(m_hMutex, INFINITE);
#else
puts("[LoadAIBuffer] To do: non-Win32 m_hMutex");
#endif
// Step 0: Replace depleted stored buffer for next run
BufferAudio();
// Step 1: Deplete stored buffer (should equal length size)
if (samplesToMove <= m_BufferRemaining)
{
Cinp0 = inp[0] / 65535.0f;
Cinp1 = inp[1] / 65535.0f;
while (samplesToMove > 0 && m_BufferRemaining > 0)
{
outp[0] = (Linp0 + accum * (Cinp0 - Linp0)) * gain;
outp[1] = (Linp1 + accum * (Cinp1 - Linp1)) * gain;
if (outp[0] > 1.0) outp[0] = 1.0; if (outp[0] < -1.0) outp[0] = -1.0;
if (outp[1] > 1.0) outp[1] = 1.0; if (outp[1] < -1.0) outp[1] = -1.0;
accum += ratio;
while (accum >= 1.0)
{
accum -= 1.0;
m_CurrentReadLoc += 4;
m_CurrentReadLoc %= m_MaxBufferSize;
inp = (s16 *)(m_Buffer + m_CurrentReadLoc);
Linp0 = Cinp0; Cinp0 = (inp[0] / 65535.0f);
Linp1 = Cinp1; Cinp1 = (inp[1] / 65535.0f);
assert(m_BufferRemaining <= m_MaxBufferSize);
m_BufferRemaining -= 4;
assert(m_BufferRemaining <= m_MaxBufferSize);
}
samplesToMove -= 1;
outp += 2;
}
}
// Step 2: Fill bytesToMove with silence
if (samplesToMove == frames)
DEBUG_OUTPUT("S");
while (samplesToMove > 0)
{
outp[0] = Linp0; outp[1] = Linp1;
outp += 2;
samplesToMove -= 1;
}
// Step 3: Replace depleted stored buffer for next run
BufferAudio();
#ifdef _WIN32
ReleaseMutex(m_hMutex);
#else
// to do
#endif
assert(samplesToMove == 0);
return (frames - samplesToMove);
}
#endif