Initial re-upload of spice2x-24-08-24
This commit is contained in:
92
hooks/audio/audio.cpp
Normal file
92
hooks/audio/audio.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "audio.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
|
||||
#include "util/detour.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/memutils.h"
|
||||
|
||||
#include "audio_private.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
0xBCDE0395, 0xE52F, 0x467C,
|
||||
0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E);
|
||||
#endif
|
||||
|
||||
// function pointers
|
||||
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
// public globals
|
||||
bool ENABLED = true;
|
||||
bool VOLUME_HOOK_ENABLED = true;
|
||||
bool USE_DUMMY = false;
|
||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||
std::optional<Backend> BACKEND = std::nullopt;
|
||||
size_t ASIO_DRIVER_ID = 0;
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
// private globals
|
||||
IAudioClient *CLIENT = nullptr;
|
||||
std::mutex INITIALIZE_LOCK; // for asio
|
||||
}
|
||||
|
||||
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
||||
REFCLSID rclsid,
|
||||
LPUNKNOWN pUnkOuter,
|
||||
DWORD dwClsContext,
|
||||
REFIID riid,
|
||||
LPVOID *ppv)
|
||||
{
|
||||
// call original
|
||||
HRESULT ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
|
||||
if (FAILED(ret)) {
|
||||
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
|
||||
log_warning("audio", "CoCreateInstance failed, hr={}", FMT_HRESULT(ret));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// check if this is the audio device enumerator
|
||||
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
|
||||
|
||||
// wrap object
|
||||
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
|
||||
*mmde = new WrappedIMMDeviceEnumerator(*mmde);
|
||||
}
|
||||
|
||||
// return original result
|
||||
return ret;
|
||||
}
|
||||
|
||||
namespace hooks::audio {
|
||||
void init() {
|
||||
if (!ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
log_info("audio", "initializing");
|
||||
init_low_latency();
|
||||
|
||||
// general hooks
|
||||
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
|
||||
}
|
||||
|
||||
void stop() {
|
||||
log_info("audio", "stopping");
|
||||
if (CLIENT) {
|
||||
CLIENT->Stop();
|
||||
CLIENT->Release();
|
||||
CLIENT = nullptr;
|
||||
}
|
||||
stop_low_latency();
|
||||
}
|
||||
}
|
||||
39
hooks/audio/audio.h
Normal file
39
hooks/audio/audio.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
#ifndef _MSC_VER
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
#endif
|
||||
|
||||
namespace hooks::audio {
|
||||
enum class Backend {
|
||||
Asio,
|
||||
WaveOut,
|
||||
};
|
||||
|
||||
extern bool ENABLED;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern bool USE_DUMMY;
|
||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||
extern std::optional<Backend> BACKEND;
|
||||
extern size_t ASIO_DRIVER_ID;
|
||||
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
|
||||
extern bool LOW_LATENCY_SHARED_WASAPI;
|
||||
|
||||
void init();
|
||||
void stop();
|
||||
|
||||
inline std::optional<Backend> name_to_backend(const char *value) {
|
||||
if (_stricmp(value, "asio") == 0) {
|
||||
return Backend::Asio;
|
||||
} else if (_stricmp(value, "waveout") == 0) {
|
||||
return Backend::WaveOut;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
17
hooks/audio/audio_private.h
Normal file
17
hooks/audio/audio_private.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <windows.h>
|
||||
#include <audioclient.h>
|
||||
#include "hooks/audio/backends/wasapi/low_latency_client.h"
|
||||
|
||||
constexpr bool AUDIO_LOG_HRESULT = true;
|
||||
|
||||
namespace hooks::audio {
|
||||
|
||||
extern IAudioClient *CLIENT;
|
||||
extern std::mutex INITIALIZE_LOCK;
|
||||
extern bool VOLUME_HOOK_ENABLED;
|
||||
extern LowLatencyAudioClient *LOW_LATENCY_CLIENT;
|
||||
}
|
||||
240
hooks/audio/backends/dsound/dsound_backend.cpp
Normal file
240
hooks/audio/backends/dsound/dsound_backend.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#include "dsound_backend.h"
|
||||
|
||||
#include <dsound.h>
|
||||
#include "util/detour.h"
|
||||
#include "util/libutils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
static void *DIRECT_SOUND_CREATE8_ADR = nullptr;
|
||||
static char DIRECT_SOUND_CREATE8_CONTENTS[16];
|
||||
static decltype(DirectSoundCreate8) *DirectSoundCreate8_orig = nullptr;
|
||||
|
||||
static HRESULT WINAPI DirectSoundCreate8_hook(LPCGUID lpGUID, LPDIRECTSOUND8 *ppDS8, LPUNKNOWN pUnkOuter) {
|
||||
log_misc("audio::dsound", "DirectSoundCreate8 hook hit");
|
||||
|
||||
// remove hook
|
||||
if (DIRECT_SOUND_CREATE8_ADR) {
|
||||
detour::inline_restore(DIRECT_SOUND_CREATE8_ADR, DIRECT_SOUND_CREATE8_CONTENTS);
|
||||
|
||||
if (DirectSoundCreate8_orig == nullptr) {
|
||||
DirectSoundCreate8_orig = reinterpret_cast<decltype(DirectSoundCreate8) *>(DIRECT_SOUND_CREATE8_ADR);
|
||||
}
|
||||
}
|
||||
|
||||
HRESULT result = DirectSoundCreate8_orig(lpGUID, ppDS8, pUnkOuter);
|
||||
if (result != DS_OK) {
|
||||
log_warning("audio::dsound", "failed to create DirectSound interface");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
*ppDS8 = new WrappedIDirectSound8(*ppDS8);
|
||||
|
||||
// add hook
|
||||
if (DIRECT_SOUND_CREATE8_ADR) {
|
||||
detour::inline_noprotect((void *) DirectSoundCreate8_hook, DIRECT_SOUND_CREATE8_ADR);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma region WrappedIDirectSound8
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::QueryInterface(const IID &riid, void **ppvObject) {
|
||||
return pReal->QueryInterface(riid, ppvObject);
|
||||
}
|
||||
|
||||
__declspec (nothrow) ULONG WINAPI WrappedIDirectSound8::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
|
||||
__declspec (nothrow) ULONG WINAPI WrappedIDirectSound8::Release() {
|
||||
return pReal->Release();
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::CreateSoundBuffer(
|
||||
LPCDSBUFFERDESC lpcDSBufferDesc,
|
||||
LPLPDIRECTSOUNDBUFFER lplpDirectSoundBuffer,
|
||||
IUnknown *pUnkOuter)
|
||||
{
|
||||
HRESULT result = pReal->CreateSoundBuffer(lpcDSBufferDesc, lplpDirectSoundBuffer, pUnkOuter);
|
||||
if (result != DS_OK) {
|
||||
log_warning("audio::dsound", "failed to create sound buffer");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
*lplpDirectSoundBuffer = new WrappedIDirectSoundBuffer(*lplpDirectSoundBuffer);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::GetCaps(LPDSCAPS lpDSCaps) {
|
||||
return pReal->GetCaps(lpDSCaps);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::DuplicateSoundBuffer(
|
||||
LPDIRECTSOUNDBUFFER lpDsbOriginal,
|
||||
LPLPDIRECTSOUNDBUFFER lplpDsbDuplicate)
|
||||
{
|
||||
return pReal->DuplicateSoundBuffer(lpDsbOriginal, lplpDsbDuplicate);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::SetCooperativeLevel(HWND hwnd, DWORD dwLevel) {
|
||||
return pReal->SetCooperativeLevel(hwnd, dwLevel);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::Compact() {
|
||||
return pReal->Compact();
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::GetSpeakerConfig(LPDWORD lpdwSpeakerConfig) {
|
||||
return pReal->GetSpeakerConfig(lpdwSpeakerConfig);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::SetSpeakerConfig(DWORD dwSpeakerConfig) {
|
||||
return pReal->SetSpeakerConfig(dwSpeakerConfig);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::Initialize(LPCGUID lpcGuid) {
|
||||
return pReal->Initialize(lpcGuid);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSound8::VerifyCertification(LPDWORD pdwCertified) {
|
||||
return pReal->VerifyCertification(pdwCertified);
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
#pragma region WrappedIDirectSoundBuffer
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::QueryInterface(const IID &riid, void **ppvObject) {
|
||||
return pReal->QueryInterface(riid, ppvObject);
|
||||
}
|
||||
|
||||
__declspec (nothrow) ULONG WINAPI WrappedIDirectSoundBuffer::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
|
||||
__declspec (nothrow) ULONG WINAPI WrappedIDirectSoundBuffer::Release() {
|
||||
return pReal->Release();
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetCaps(LPDSBCAPS lpDSBufferCaps) {
|
||||
return pReal->GetCaps(lpDSBufferCaps);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetCurrentPosition(
|
||||
LPDWORD lpdwCurrentPlayCursor,
|
||||
LPDWORD lpdwCurrentWriteCursor)
|
||||
{
|
||||
return pReal->GetCurrentPosition(lpdwCurrentPlayCursor, lpdwCurrentWriteCursor);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetFormat(
|
||||
LPWAVEFORMATEX lpwfxFormat,
|
||||
DWORD dwSizeAllocated,
|
||||
LPDWORD lpdwSizeWritten)
|
||||
{
|
||||
return pReal->GetFormat(lpwfxFormat, dwSizeAllocated, lpdwSizeWritten);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetVolume(LPLONG lplVolume) {
|
||||
return pReal->GetVolume(lplVolume);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetPan(LPLONG lplpan) {
|
||||
return pReal->GetPan(lplpan);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetFrequency(LPDWORD lpdwFrequency) {
|
||||
return pReal->GetFrequency(lpdwFrequency);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::GetStatus(LPDWORD lpdwStatus) {
|
||||
return pReal->GetStatus(lpdwStatus);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Initialize(
|
||||
LPDIRECTSOUND lpDirectSound,
|
||||
LPCDSBUFFERDESC lpcDSBufferDesc)
|
||||
{
|
||||
return pReal->Initialize(lpDirectSound, lpcDSBufferDesc);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Lock(
|
||||
DWORD dwOffset,
|
||||
DWORD dwBytes,
|
||||
LPVOID *ppvAudioPtr1,
|
||||
LPDWORD pdwAudioBytes1,
|
||||
LPVOID *ppvAudioPtr2,
|
||||
LPDWORD pdwAudioBytes2,
|
||||
DWORD dwFlags)
|
||||
{
|
||||
return pReal->Lock(dwOffset, dwBytes, ppvAudioPtr1, pdwAudioBytes1, ppvAudioPtr2, pdwAudioBytes2, dwFlags);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Play(
|
||||
DWORD dwReserved1,
|
||||
DWORD dwReserved2,
|
||||
DWORD dwFlags)
|
||||
{
|
||||
return pReal->Play(dwReserved1, dwReserved2, dwFlags);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::SetCurrentPosition(DWORD dwNewPosition) {
|
||||
return pReal->SetCurrentPosition(dwNewPosition);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::SetFormat(LPCWAVEFORMATEX lpcfxFormat) {
|
||||
HRESULT result = pReal->SetFormat(lpcfxFormat);
|
||||
// for KBR, MBR
|
||||
if (result == DSERR_ALLOCATED) {
|
||||
log_info("audio::dsound", "WrappedIDirectSoundBuffer::SetFormat returned DSERR_ALLOCATED");
|
||||
return DS_OK;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::SetVolume(LONG lVolume) {
|
||||
return pReal->SetVolume(lVolume);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::SetPan(LONG lPan) {
|
||||
return pReal->SetPan(lPan);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::SetFrequency(DWORD dwFrequency) {
|
||||
return pReal->SetFrequency(dwFrequency);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Stop() {
|
||||
return pReal->Stop();
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Unlock(
|
||||
LPVOID pvAudioPtr1,
|
||||
DWORD dwAudioBytes1,
|
||||
LPVOID pvAudioPtr2,
|
||||
DWORD dwAudioPtr2)
|
||||
{
|
||||
return pReal->Unlock(pvAudioPtr1, dwAudioBytes1, pvAudioPtr2, dwAudioPtr2);
|
||||
}
|
||||
|
||||
__declspec (nothrow) HRESULT WINAPI WrappedIDirectSoundBuffer::Restore() {
|
||||
return pReal->Restore();
|
||||
}
|
||||
#pragma endregion
|
||||
|
||||
void audio_dsound_init() {
|
||||
log_info("audio::dsound", "initializing");
|
||||
|
||||
// dsound inline hooks
|
||||
HMODULE dsound = libutils::try_module("dsound.dll");
|
||||
if (!dsound) {
|
||||
log_info("audio::dsound", "skipping inline hooks");
|
||||
} else {
|
||||
DIRECT_SOUND_CREATE8_ADR = (void *) libutils::get_proc(dsound, "DirectSoundCreate8");
|
||||
detour::inline_preserve(reinterpret_cast<void *>(DirectSoundCreate8_hook), DIRECT_SOUND_CREATE8_ADR, DIRECT_SOUND_CREATE8_CONTENTS);
|
||||
}
|
||||
|
||||
DirectSoundCreate8_orig = detour::iat_try("DirectSoundCreate8", DirectSoundCreate8_hook);
|
||||
}
|
||||
76
hooks/audio/backends/dsound/dsound_backend.h
Normal file
76
hooks/audio/backends/dsound/dsound_backend.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmsystem.h>
|
||||
#include <dsound.h>
|
||||
|
||||
void audio_dsound_init();
|
||||
|
||||
struct WrappedIDirectSound8 : IDirectSound8 {
|
||||
explicit WrappedIDirectSound8(IDirectSound8 *orig) : pReal(orig) {}
|
||||
|
||||
WrappedIDirectSound8(const WrappedIDirectSound8 &) = delete;
|
||||
WrappedIDirectSound8 &operator=(const WrappedIDirectSound8 &) = delete;
|
||||
|
||||
virtual ~WrappedIDirectSound8() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
__declspec (nothrow) HRESULT WINAPI QueryInterface(REFIID riid, void** ppvObject) override;
|
||||
__declspec (nothrow) ULONG WINAPI AddRef() override;
|
||||
__declspec (nothrow) ULONG WINAPI Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IDirectSound8 methods
|
||||
__declspec (nothrow) HRESULT WINAPI CreateSoundBuffer(LPCDSBUFFERDESC lpcDSBufferDesc, LPLPDIRECTSOUNDBUFFER lplpDirectSoundBuffer, IUnknown *pUnkOuter) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetCaps(LPDSCAPS lpDSCaps) override;
|
||||
__declspec (nothrow) HRESULT WINAPI DuplicateSoundBuffer(LPDIRECTSOUNDBUFFER lpDsbOriginal, LPLPDIRECTSOUNDBUFFER lplpDsbDuplicate) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetCooperativeLevel(HWND hwnd, DWORD dwLevel) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Compact() override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetSpeakerConfig(LPDWORD lpdwSpeakerConfig) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetSpeakerConfig(DWORD dwSpeakerConfig) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Initialize(LPCGUID lpcGuid) override;
|
||||
__declspec (nothrow) HRESULT WINAPI VerifyCertification(LPDWORD pdwCertified) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IDirectSound8 *pReal;
|
||||
};
|
||||
|
||||
struct WrappedIDirectSoundBuffer : IDirectSoundBuffer {
|
||||
explicit WrappedIDirectSoundBuffer(IDirectSoundBuffer *orig) : pReal(orig) {}
|
||||
|
||||
WrappedIDirectSoundBuffer(const WrappedIDirectSound8 &) = delete;
|
||||
WrappedIDirectSoundBuffer &operator=(const WrappedIDirectSoundBuffer &) = delete;
|
||||
|
||||
virtual ~WrappedIDirectSoundBuffer() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
__declspec (nothrow) HRESULT WINAPI QueryInterface(REFIID riid, void** ppvObject) override;
|
||||
__declspec (nothrow) ULONG WINAPI AddRef() override;
|
||||
__declspec (nothrow) ULONG WINAPI Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IDirectSoundBuffer methods
|
||||
__declspec (nothrow) HRESULT WINAPI GetCaps(LPDSBCAPS lpDSBufferCaps) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetCurrentPosition(LPDWORD lpdwCurrentPlayCursor, LPDWORD lpdwCurrentWriteCursor) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetFormat(LPWAVEFORMATEX lpwfxFormat, DWORD dwSizeAllocated, LPDWORD lpdwSizeWritten) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetVolume(LPLONG lplVolume) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetPan(LPLONG lplpan) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetFrequency(LPDWORD lpdwFrequency) override;
|
||||
__declspec (nothrow) HRESULT WINAPI GetStatus(LPDWORD lpdwStatus) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Initialize(LPDIRECTSOUND lpDirectSound, LPCDSBUFFERDESC lpcDSBufferDesc) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Lock(DWORD dwOffset, DWORD dwBytes, LPVOID *ppvAudioPtr1, LPDWORD pdwAudioBytes1, LPVOID *ppvAudioPtr2, LPDWORD pdwAudioBytes2, DWORD dwFlags) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Play(DWORD dwReserved1, DWORD dwReserved2, DWORD dwFlags) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetCurrentPosition(DWORD dwNewPosition) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetFormat(LPCWAVEFORMATEX lpcfxFormat) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetVolume(LONG lVolume) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetPan(LONG lPan) override;
|
||||
__declspec (nothrow) HRESULT WINAPI SetFrequency(DWORD dwFrequency) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Stop() override;
|
||||
__declspec (nothrow) HRESULT WINAPI Unlock(LPVOID pvAudioPtr1, DWORD dwAudioBytes1, LPVOID pvAudioPtr2, DWORD dwAudioPtr2) override;
|
||||
__declspec (nothrow) HRESULT WINAPI Restore() override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IDirectSoundBuffer *pReal;
|
||||
};
|
||||
112
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
Normal file
112
hooks/audio/backends/mmdevice/audio_endpoint_volume.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "audio_endpoint_volume.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == __uuidof(IAudioEndpointVolume)) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->QueryInterface(riid, ppvObj);
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioEndpointVolume::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioEndpointVolume::Release() {
|
||||
|
||||
// get reference count of underlying interface
|
||||
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::SetMasterVolumeLevelScalar called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) {
|
||||
return pReal->RegisterControlChangeNotify(pNotify);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) {
|
||||
return pReal->UnregisterControlChangeNotify(pNotify);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetChannelCount(uint32_t *pnChannelCount) {
|
||||
return pReal->GetChannelCount(pnChannelCount);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::SetMasterVolumeLevel called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetMasterVolumeLevel(float *fLevelDB) {
|
||||
return pReal->GetMasterVolumeLevel(fLevelDB);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetMasterVolumeLevelScalar(float *fLevel) {
|
||||
return pReal->GetMasterVolumeLevelScalar(fLevel);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::SetChannelVolumeLevel called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::SetChannelVolumeLevelScalar called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) {
|
||||
return pReal->GetChannelVolumeLevel(nChannel, fLevelDB);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) {
|
||||
return pReal->GetChannelVolumeLevelScalar(nChannel, fLevel);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::SetMute(WINBOOL bMute, LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::SetMute called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetMute(WINBOOL *bMute) {
|
||||
return pReal->GetMute(bMute);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) {
|
||||
return pReal->GetVolumeStepInfo(pnStep, pnStepCount);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::VolumeStepUp(LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::VolumeStepUp called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::VolumeStepDown(LPCGUID pguidEventContext) {
|
||||
log_misc("audio", "WrappedIAudioEndpointVolume::VolumeStepDown called; ignoring volume change");
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::QueryHardwareSupport(DWORD *pdwHardwareSupportMask) {
|
||||
return pReal->QueryHardwareSupport(pdwHardwareSupportMask);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioEndpointVolume::GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) {
|
||||
return pReal->GetVolumeRange(pflVolumeMindB, pflVolumeMaxdB, pflVolumeIncrementdB);
|
||||
}
|
||||
43
hooks/audio/backends/mmdevice/audio_endpoint_volume.h
Normal file
43
hooks/audio/backends/mmdevice/audio_endpoint_volume.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <endpointvolume.h>
|
||||
|
||||
struct WrappedIAudioEndpointVolume : IAudioEndpointVolume {
|
||||
explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {}
|
||||
|
||||
WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete;
|
||||
WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete;
|
||||
|
||||
virtual ~WrappedIAudioEndpointVolume() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioEndpointVolume
|
||||
HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override;
|
||||
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override;
|
||||
HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override;
|
||||
HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IAudioEndpointVolume *const pReal;
|
||||
};
|
||||
128
hooks/audio/backends/mmdevice/device.cpp
Normal file
128
hooks/audio/backends/mmdevice/device.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
#include "device.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <audioclient.h>
|
||||
#include <endpointvolume.h>
|
||||
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "hooks/audio/backends/mmdevice/audio_endpoint_volume.h"
|
||||
#include "hooks/audio/backends/wasapi/audio_client.h"
|
||||
|
||||
#define PRINT_FAILED_RESULT(name, ret) \
|
||||
do { \
|
||||
if (AUDIO_LOG_HRESULT) { \
|
||||
log_warning("audio::mmdevice", "{} failed, hr={}", name, FMT_HRESULT(ret)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_RESULT(x) \
|
||||
do { \
|
||||
HRESULT __ret = (x); \
|
||||
if (FAILED(__ret)) { \
|
||||
PRINT_FAILED_RESULT(__FUNCTION__, __ret); \
|
||||
} \
|
||||
return __ret; \
|
||||
} while (0)
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(IID_IMMDevice,
|
||||
0xd666063f, 0x1587, 0x4e43,
|
||||
0x81, 0xf1, 0xb9, 0x48, 0xe8, 0x07, 0x36, 0x3f);
|
||||
#endif
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_WrappedIMMDevice ||
|
||||
riid == IID_IMMDevice)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->QueryInterface(riid, ppvObj);
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIMMDevice::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIMMDevice::Release() {
|
||||
|
||||
// get reference count of underlying interface
|
||||
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDevice::Activate(
|
||||
REFIID iid,
|
||||
DWORD dwClsCtx,
|
||||
PROPVARIANT *pActivationParams,
|
||||
void **ppInterface)
|
||||
{
|
||||
log_misc("audio::mmdevice", "WrappedIMMDevice::Activate");
|
||||
|
||||
// call original
|
||||
HRESULT ret = pReal->Activate(iid, dwClsCtx, pActivationParams, ppInterface);
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IMMDevice::Activate", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (iid == IID_IAudioClient) {
|
||||
|
||||
// prevent initialization recursion when using some ASIO backends that proxy to DirectSound, WASAPI, or WDM
|
||||
// like ASIO4All or FlexASIO
|
||||
if (!hooks::audio::INITIALIZE_LOCK.try_lock()) {
|
||||
log_warning("audio::mmdevice", "ignoring wrap request while backend is initializing, possible recursion");
|
||||
return ret;
|
||||
}
|
||||
std::lock_guard initialize_guard(hooks::audio::INITIALIZE_LOCK, std::adopt_lock);
|
||||
|
||||
auto client = reinterpret_cast<IAudioClient *>(*ppInterface);
|
||||
|
||||
// release old audio client if initialized
|
||||
if (hooks::audio::CLIENT) {
|
||||
hooks::audio::CLIENT->Release();
|
||||
}
|
||||
|
||||
/*
|
||||
ret = wrap_audio_client(pReal, dwClsCtx, pActivationParams, &client);
|
||||
if (FAILED(ret)) {
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
client = wrap_audio_client(client);
|
||||
|
||||
*ppInterface = client;
|
||||
|
||||
// persist the audio client
|
||||
hooks::audio::CLIENT = client;
|
||||
hooks::audio::CLIENT->AddRef();
|
||||
|
||||
} else if (iid == __uuidof(IAudioEndpointVolume) && hooks::audio::VOLUME_HOOK_ENABLED) {
|
||||
*ppInterface = new WrappedIAudioEndpointVolume(reinterpret_cast<IAudioEndpointVolume *>(*ppInterface));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDevice::OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) {
|
||||
CHECK_RESULT(pReal->OpenPropertyStore(stgmAccess, ppProperties));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDevice::GetId(LPWSTR *ppstrId) {
|
||||
CHECK_RESULT(pReal->GetId(ppstrId));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDevice::GetState(DWORD *pdwState) {
|
||||
CHECK_RESULT(pReal->GetState(pdwState));
|
||||
}
|
||||
36
hooks/audio/backends/mmdevice/device.h
Normal file
36
hooks/audio/backends/mmdevice/device.h
Normal file
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
// {7CC2A363-D96F-4BE2-B6CF-2A44AADA424B}
|
||||
static const GUID IID_WrappedIMMDevice = {
|
||||
0x7cc2a363, 0xd96f, 0x4be2, { 0xb6, 0xcf, 0x2a, 0x44, 0xaa, 0xda, 0x42, 0x4b }
|
||||
};
|
||||
|
||||
struct WrappedIMMDevice : IMMDevice {
|
||||
explicit WrappedIMMDevice(IMMDevice *orig) : pReal(orig) {
|
||||
}
|
||||
|
||||
WrappedIMMDevice(const WrappedIMMDevice &) = delete;
|
||||
WrappedIMMDevice &operator=(const WrappedIMMDevice &) = delete;
|
||||
|
||||
virtual ~WrappedIMMDevice() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDevice
|
||||
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
|
||||
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
|
||||
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
|
||||
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
|
||||
#pragma endregion
|
||||
|
||||
IMMDevice *const pReal;
|
||||
};
|
||||
82
hooks/audio/backends/mmdevice/device_enumerator.cpp
Normal file
82
hooks/audio/backends/mmdevice/device_enumerator.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "device_enumerator.h"
|
||||
#include "device.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(IID_IMMDeviceEnumerator,
|
||||
0xa95664d2, 0x9614, 0x4f35,
|
||||
0xa7, 0x46, 0xde, 0x8d, 0xb6, 0x36, 0x17, 0xe6);
|
||||
#endif
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_IMMDeviceEnumerator) {
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->QueryInterface(riid, ppvObj);
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::Release() {
|
||||
|
||||
// get reference count of underlying interface
|
||||
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
|
||||
EDataFlow dataFlow,
|
||||
DWORD dwStateMask,
|
||||
IMMDeviceCollection **ppDevices)
|
||||
{
|
||||
return pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::GetDefaultAudioEndpoint(
|
||||
EDataFlow dataFlow,
|
||||
ERole role,
|
||||
IMMDevice **ppEndpoint)
|
||||
{
|
||||
// call orignal
|
||||
HRESULT ret = this->pReal->GetDefaultAudioEndpoint(dataFlow, role, ppEndpoint);
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
log_warning("audio", "IMMDeviceEnumerator::GetDefaultAudioEndpoint failed, hr={}", FMT_HRESULT(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
// wrap interface
|
||||
*ppEndpoint = new WrappedIMMDevice(*ppEndpoint);
|
||||
|
||||
// return original result
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::GetDevice(
|
||||
LPCWSTR pwstrId,
|
||||
IMMDevice **ppDevice)
|
||||
{
|
||||
return pReal->GetDevice(pwstrId, ppDevice);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::RegisterEndpointNotificationCallback(
|
||||
IMMNotificationClient *pClient)
|
||||
{
|
||||
return pReal->RegisterEndpointNotificationCallback(pClient);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::UnregisterEndpointNotificationCallback(
|
||||
IMMNotificationClient *pClient)
|
||||
{
|
||||
return pReal->UnregisterEndpointNotificationCallback(pClient);
|
||||
}
|
||||
33
hooks/audio/backends/mmdevice/device_enumerator.h
Normal file
33
hooks/audio/backends/mmdevice/device_enumerator.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "util/logging.h"
|
||||
|
||||
struct WrappedIMMDeviceEnumerator : IMMDeviceEnumerator {
|
||||
explicit WrappedIMMDeviceEnumerator(IMMDeviceEnumerator *orig) : pReal(orig) {
|
||||
}
|
||||
|
||||
WrappedIMMDeviceEnumerator(const WrappedIMMDeviceEnumerator &) = delete;
|
||||
WrappedIMMDeviceEnumerator &operator=(const WrappedIMMDeviceEnumerator &) = delete;
|
||||
|
||||
virtual ~WrappedIMMDeviceEnumerator() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
virtual ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
virtual ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IMMDeviceEnumerator
|
||||
virtual HRESULT STDMETHODCALLTYPE EnumAudioEndpoints(EDataFlow dataFlow, DWORD dwStateMask, IMMDeviceCollection **ppDevices) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, IMMDevice **ppEndpoint) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetDevice(LPCWSTR pwstrId, IMMDevice **ppDevice) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE RegisterEndpointNotificationCallback(IMMNotificationClient *pClient) override;
|
||||
virtual HRESULT STDMETHODCALLTYPE UnregisterEndpointNotificationCallback(IMMNotificationClient *pClient) override;
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
IMMDeviceEnumerator *const pReal;
|
||||
};
|
||||
484
hooks/audio/backends/wasapi/audio_client.cpp
Normal file
484
hooks/audio/backends/wasapi/audio_client.cpp
Normal file
@@ -0,0 +1,484 @@
|
||||
#include "audio_client.h"
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "avs/game.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/util.h"
|
||||
#include "hooks/audio/backends/wasapi/util.h"
|
||||
#include "hooks/audio/implementations/asio.h"
|
||||
#include "hooks/audio/implementations/wave_out.h"
|
||||
//#include "util/co_task_mem_ptr.h"
|
||||
|
||||
#include "defs.h"
|
||||
#include "dummy_audio_client.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
#if 0
|
||||
#define WRAP_DEBUG log_misc("audio::wasapi", "{}::{}", CLASS_NAME, __func__)
|
||||
#define WRAP_DEBUG_FMT(format, ...) log_misc("audio::wasapi", format, __VA_ARGS__)
|
||||
#else
|
||||
#define WRAP_DEBUG do {} while (0)
|
||||
#define WRAP_DEBUG_FMT(format, ...) do {} while (0)
|
||||
#endif
|
||||
|
||||
#if 1
|
||||
#define WRAP_VERBOSE log_misc("audio::wasapi", "{}::{}", CLASS_NAME, __func__)
|
||||
#else
|
||||
#define WRAP_VERBOSE do {} while (0)
|
||||
#endif
|
||||
|
||||
const char CLASS_NAME[] = "WrappedIAudioClient";
|
||||
|
||||
static void fix_rec_format(WAVEFORMATEX *pFormat) {
|
||||
log_misc("audio::wasapi", "changing format to 2ch 16-bit");
|
||||
|
||||
pFormat->nChannels = 2;
|
||||
pFormat->wBitsPerSample = 16;
|
||||
pFormat->nBlockAlign = pFormat->nChannels * (pFormat->wBitsPerSample / 8);
|
||||
pFormat->nAvgBytesPerSec = pFormat->nSamplesPerSec * pFormat->nBlockAlign;
|
||||
}
|
||||
|
||||
// TODO(felix): is it appropriate to automatically switch to shared mode? should we do a
|
||||
// `MessageBox` to notify the user?
|
||||
/*
|
||||
static bool check_for_exclusive_access(IAudioClient *client) {
|
||||
static bool checked_once = false;
|
||||
static bool previous_check_result = false;
|
||||
|
||||
CoTaskMemPtr<WAVEFORMATEX> mix_format;
|
||||
REFERENCE_TIME requested_duration = 0;
|
||||
|
||||
if (checked_once) {
|
||||
return previous_check_result;
|
||||
}
|
||||
if (audio::BACKEND.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// scope function so it has access to the local static variables
|
||||
auto set_result = [](bool result) {
|
||||
checked_once = true;
|
||||
previous_check_result = result;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
HRESULT ret = client->GetMixFormat(mix_format.ppv());
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient::GetMixFormat", ret);
|
||||
return set_result(false);
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "Mix Format:");
|
||||
print_format(mix_format.data());
|
||||
|
||||
ret = client->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, mix_format.data(), nullptr);
|
||||
if (ret == AUDCLNT_E_UNSUPPORTED_FORMAT) {
|
||||
auto mix_format_ex = reinterpret_cast<WAVEFORMATEXTENSIBLE *>(mix_format.data());
|
||||
|
||||
log_warning("audio::wasapi", "device does not natively support the mix format, converting to PCM");
|
||||
|
||||
if (mix_format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
|
||||
IsEqualGUID(GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, mix_format_ex->SubFormat))
|
||||
{
|
||||
mix_format_ex->Format.wBitsPerSample = 16;
|
||||
mix_format_ex->Format.nBlockAlign = mix_format_ex->Format.nChannels * (mix_format_ex->Format.wBitsPerSample / 8);
|
||||
mix_format_ex->Format.nAvgBytesPerSec = mix_format_ex->Format.nSamplesPerSec * mix_format_ex->Format.nBlockAlign;
|
||||
mix_format_ex->Samples.wValidBitsPerSample = 16;
|
||||
mix_format_ex->SubFormat = GUID_KSDATAFORMAT_SUBTYPE_PCM;
|
||||
} else if (mix_format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
|
||||
mix_format->wBitsPerSample = 16;
|
||||
mix_format->nBlockAlign = mix_format->nChannels * (mix_format->wBitsPerSample / 8);
|
||||
mix_format->nAvgBytesPerSec = mix_format->nSamplesPerSec * mix_format->nBlockAlign;
|
||||
mix_format->wFormatTag = WAVE_FORMAT_PCM;
|
||||
} else {
|
||||
log_warning("audio::wasapi", "mix format is not a floating point format");
|
||||
return set_result(false);
|
||||
}
|
||||
|
||||
ret = client->IsFormatSupported(AUDCLNT_SHAREMODE_EXCLUSIVE, mix_format.data(), nullptr);
|
||||
if (FAILED(ret)) {
|
||||
log_warning("audio::wasapi", "mix format is not supported");
|
||||
return set_result(false);
|
||||
}
|
||||
}
|
||||
|
||||
ret = client->GetDevicePeriod(nullptr, &requested_duration);
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient::GetDevicePeriod", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
ret = client->Initialize(
|
||||
AUDCLNT_SHAREMODE_EXCLUSIVE,
|
||||
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
|
||||
requested_duration,
|
||||
requested_duration,
|
||||
mix_format.data(),
|
||||
nullptr);
|
||||
|
||||
if (ret == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED || SUCCEEDED(ret)) {
|
||||
log_info("audio::wasapi", "exclusive mode is available, disabling backend");
|
||||
return set_result(true);
|
||||
} else {
|
||||
log_warning("audio::wasapi", "exclusive mode is not available, enabling backend, hr={}", FMT_HRESULT(ret));
|
||||
}
|
||||
|
||||
return set_result(false);
|
||||
}
|
||||
|
||||
HRESULT wrap_audio_client(
|
||||
IMMDevice *device,
|
||||
DWORD cls_ctx,
|
||||
PROPVARIANT *activation_params,
|
||||
IAudioClient **audio_client)
|
||||
{
|
||||
auto exclusive_available = check_for_exclusive_access(*audio_client);
|
||||
(*audio_client)->Stop();
|
||||
(*audio_client)->Reset();
|
||||
(*audio_client)->Release();
|
||||
*audio_client = nullptr;
|
||||
|
||||
SAFE_CALL("IMMDevice", "Activate", device->Activate(
|
||||
IID_IAudioClient,
|
||||
cls_ctx,
|
||||
activation_params,
|
||||
reinterpret_cast<void **>(audio_client)));
|
||||
*/
|
||||
IAudioClient *wrap_audio_client(IAudioClient *audio_client) {
|
||||
AudioBackend *backend = nullptr;
|
||||
bool requires_dummy = false;
|
||||
|
||||
if (hooks::audio::BACKEND.has_value()) {
|
||||
switch (hooks::audio::BACKEND.value()) {
|
||||
case hooks::audio::Backend::Asio:
|
||||
backend = new AsioBackend();
|
||||
requires_dummy = true;
|
||||
break;
|
||||
case hooks::audio::Backend::WaveOut:
|
||||
backend = new WaveOutBackend();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
//} else if (!exclusive_available) {
|
||||
// backend = new WaveOutBackend();
|
||||
//}
|
||||
|
||||
IAudioClient *new_client;
|
||||
|
||||
if (hooks::audio::USE_DUMMY || requires_dummy) {
|
||||
|
||||
// release the old context since it is not used by the dummy context
|
||||
audio_client->Release();
|
||||
|
||||
new_client = new DummyIAudioClient(backend);
|
||||
} else {
|
||||
new_client = new WrappedIAudioClient(audio_client, backend);
|
||||
}
|
||||
|
||||
return new_client;
|
||||
}
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_WrappedIAudioClient ||
|
||||
riid == IID_IAudioClient)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->QueryInterface(riid, ppvObj);
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioClient::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioClient::Release() {
|
||||
|
||||
// get reference count of underlying interface
|
||||
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Initialize(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
DWORD StreamFlags,
|
||||
REFERENCE_TIME hnsBufferDuration,
|
||||
REFERENCE_TIME hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!pFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// check if format needs to be fixed
|
||||
if (pFormat->nChannels > 2 && avs::game::is_model("REC")) {
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
// verbose output
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
|
||||
if (this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_initialize", this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
|
||||
log_info("audio::wasapi", "AudioBackend::on_initialize call finished");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
}
|
||||
|
||||
// check for exclusive mode
|
||||
if (ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE) {
|
||||
this->exclusive_mode = true;
|
||||
this->frame_size = pFormat->nChannels * (pFormat->wBitsPerSample / 8);
|
||||
}
|
||||
|
||||
// call next
|
||||
HRESULT ret = pReal->Initialize(
|
||||
ShareMode,
|
||||
StreamFlags,
|
||||
hnsBufferDuration,
|
||||
hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid);
|
||||
|
||||
// check for failure
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient", "Initialize", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize success, hr={}", FMT_HRESULT(ret));
|
||||
|
||||
/*
|
||||
if (ShareMode == AUDCLNT_SHAREMODE_SHARED) {
|
||||
IAudioClockAdjustment *clock = nullptr;
|
||||
|
||||
SAFE_CALL("IAudioClient", "GetService", pReal->GetService(
|
||||
IID_IAudioClockAdjustment,
|
||||
reinterpret_cast<void **>(&clock)));
|
||||
|
||||
SAFE_CALL("IAudioClockAdjustment", "SetSampleRate", clock->SetSampleRate(
|
||||
static_cast<float>(pFormat->nSamplesPerSec)));
|
||||
}
|
||||
*/
|
||||
|
||||
copy_wave_format(&hooks::audio::FORMAT, pFormat);
|
||||
|
||||
return ret;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioClient::GetBufferSize");
|
||||
});
|
||||
|
||||
if (this->backend) {
|
||||
uint32_t buffer_frames = 0;
|
||||
|
||||
SAFE_CALL("AudioBackend", "on_get_buffer_size", this->backend->on_get_buffer_size(&buffer_frames));
|
||||
|
||||
if (buffer_frames > 0) {
|
||||
*pNumBufferFrames = buffer_frames;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetBufferSize(pNumBufferFrames));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioClient::GetStreamLatency");
|
||||
});
|
||||
|
||||
if (this->backend) {
|
||||
REFERENCE_TIME latency = 0;
|
||||
|
||||
SAFE_CALL("AudioBackend", "on_get_stream_latency", this->backend->on_get_stream_latency(
|
||||
&latency));
|
||||
|
||||
if (latency > 0) {
|
||||
*phnsLatency = latency;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetStreamLatency(phnsLatency));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetCurrentPadding(UINT32 *pNumPaddingFrames) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioClient::GetCurrentPadding");
|
||||
});
|
||||
|
||||
if (pNumPaddingFrames && this->backend) {
|
||||
std::optional<uint32_t> padding_frames;
|
||||
|
||||
SAFE_CALL("AudioBackend", "on_get_current_padding",this->backend->on_get_current_padding(
|
||||
padding_frames));
|
||||
|
||||
if (padding_frames.has_value()) {
|
||||
*pNumPaddingFrames = padding_frames.value();
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetCurrentPadding(pNumPaddingFrames));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch)
|
||||
{
|
||||
WRAP_VERBOSE;
|
||||
|
||||
if (!pFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// check if format needs to be fixed
|
||||
if (avs::game::is_model("REC") && pFormat->nChannels > 2) {
|
||||
fix_rec_format(const_cast<WAVEFORMATEX *>(pFormat));
|
||||
}
|
||||
|
||||
if (this->backend) {
|
||||
HRESULT ret = this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch);
|
||||
|
||||
if (SUCCEEDED(ret)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// return errors other than unsupported format
|
||||
if (ret != AUDCLNT_E_UNSUPPORTED_FORMAT) {
|
||||
SAFE_CALL("AudioBackend", "on_is_format_supported", ret);
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->IsFormatSupported(ShareMode, pFormat, ppClosestMatch));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetMixFormat(WAVEFORMATEX **ppDeviceFormat) {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
if (!ppDeviceFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (this->backend) {
|
||||
HRESULT ret = this->backend->on_get_mix_format(ppDeviceFormat);
|
||||
|
||||
if (SUCCEEDED(ret)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// return errors other than E_NOTIMPL
|
||||
if (ret != E_NOTIMPL) {
|
||||
SAFE_CALL("AudioBackend", "on_get_mix_format", ret);
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->GetMixFormat(ppDeviceFormat));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetDevicePeriod(
|
||||
REFERENCE_TIME *phnsDefaultDevicePeriod,
|
||||
REFERENCE_TIME *phnsMinimumDevicePeriod)
|
||||
{
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioClient::GetDevicePeriod");
|
||||
});
|
||||
|
||||
HRESULT ret = pReal->GetDevicePeriod(phnsDefaultDevicePeriod, phnsMinimumDevicePeriod);
|
||||
|
||||
if (SUCCEEDED(ret) && this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_get_device_period", this->backend->on_get_device_period(
|
||||
phnsDefaultDevicePeriod,
|
||||
phnsMinimumDevicePeriod));
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Start() {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
HRESULT ret = pReal->Start();
|
||||
|
||||
if (SUCCEEDED(ret) && this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_start", this->backend->on_start());
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Stop() {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
HRESULT ret = pReal->Stop();
|
||||
|
||||
if (SUCCEEDED(ret) && this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_stop", this->backend->on_stop());
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::Reset() {
|
||||
WRAP_VERBOSE;
|
||||
CHECK_RESULT(pReal->Reset());
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::SetEventHandle(HANDLE eventHandle) {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
if (this->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_set_event_handle", this->backend->on_set_event_handle(&eventHandle));
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->SetEventHandle(eventHandle));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioClient::GetService(REFIID riid, void **ppv) {
|
||||
WRAP_DEBUG_FMT("WrappedIAudioClient::GetService({})", guid2s(riid));
|
||||
|
||||
HRESULT ret = pReal->GetService(riid, ppv);
|
||||
|
||||
if (SUCCEEDED(ret) && ppv && *ppv && riid == IID_IAudioRenderClient) {
|
||||
auto render_client = reinterpret_cast<IAudioRenderClient *>(*ppv);
|
||||
|
||||
*ppv = new WrappedIAudioRenderClient(this, render_client);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
55
hooks/audio/backends/wasapi/audio_client.h
Normal file
55
hooks/audio/backends/wasapi/audio_client.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "hooks/audio/implementations/backend.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#include "audio_render_client.h"
|
||||
|
||||
// {1FBC8530-AF3E-4128-B418-115DE72F76B6}
|
||||
static const GUID IID_WrappedIAudioClient = {
|
||||
0x1fbc8530, 0xaf3e, 0x4128, { 0xb4, 0x18, 0x11, 0x5d, 0xe7, 0x2f, 0x76, 0xb6 }
|
||||
};
|
||||
|
||||
IAudioClient *wrap_audio_client(IAudioClient *client);
|
||||
|
||||
struct WrappedIAudioClient : IAudioClient {
|
||||
explicit WrappedIAudioClient(IAudioClient *orig, AudioBackend *backend) : pReal(orig), backend(backend) {
|
||||
}
|
||||
|
||||
WrappedIAudioClient(const WrappedIAudioClient &) = delete;
|
||||
WrappedIAudioClient &operator=(const WrappedIAudioClient &) = delete;
|
||||
|
||||
virtual ~WrappedIAudioClient() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE Initialize(AUDCLNT_SHAREMODE ShareMode, DWORD StreamFlags, REFERENCE_TIME hnsBufferDuration, REFERENCE_TIME hnsPeriodicity, const WAVEFORMATEX *pFormat, LPCGUID AudioSessionGuid) override;
|
||||
HRESULT STDMETHODCALLTYPE GetBufferSize(UINT32 *pNumBufferFrames) override;
|
||||
HRESULT STDMETHODCALLTYPE GetStreamLatency(REFERENCE_TIME *phnsLatency) override;
|
||||
HRESULT STDMETHODCALLTYPE GetCurrentPadding(UINT32 *pNumPaddingFrames) override;
|
||||
HRESULT STDMETHODCALLTYPE IsFormatSupported(AUDCLNT_SHAREMODE ShareMode, const WAVEFORMATEX *pFormat, WAVEFORMATEX **ppClosestMatch) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMixFormat(WAVEFORMATEX **ppDeviceFormat) override;
|
||||
HRESULT STDMETHODCALLTYPE GetDevicePeriod(REFERENCE_TIME *phnsDefaultDevicePeriod, REFERENCE_TIME *phnsMinimumDevicePeriod) override;
|
||||
HRESULT STDMETHODCALLTYPE Start() override;
|
||||
HRESULT STDMETHODCALLTYPE Stop() override;
|
||||
HRESULT STDMETHODCALLTYPE Reset() override;
|
||||
HRESULT STDMETHODCALLTYPE SetEventHandle(HANDLE eventHandle) override;
|
||||
HRESULT STDMETHODCALLTYPE GetService(REFIID riid, void **ppv) override;
|
||||
#pragma endregion
|
||||
|
||||
IAudioClient *const pReal;
|
||||
AudioBackend *const backend;
|
||||
|
||||
bool exclusive_mode = false;
|
||||
int frame_size = 0;
|
||||
};
|
||||
88
hooks/audio/backends/wasapi/audio_render_client.cpp
Normal file
88
hooks/audio/backends/wasapi/audio_render_client.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "audio_render_client.h"
|
||||
|
||||
#include "audio_client.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
|
||||
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_WrappedIAudioRenderClient ||
|
||||
riid == IID_IAudioRenderClient)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return pReal->QueryInterface(riid, ppvObj);
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioRenderClient::AddRef() {
|
||||
return pReal->AddRef();
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE WrappedIAudioRenderClient::Release() {
|
||||
|
||||
// get reference count of underlying interface
|
||||
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioRenderClient
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesRequested, BYTE **ppData) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioRenderClient::GetBuffer");
|
||||
});
|
||||
|
||||
if (this->client->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_get_buffer", this->client->backend->on_get_buffer(
|
||||
NumFramesRequested,
|
||||
ppData));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// call original
|
||||
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
||||
|
||||
// store buffer reference
|
||||
if (SUCCEEDED(ret)) {
|
||||
this->audio_buffer = *ppData;
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFramesWritten, DWORD dwFlags) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "WrappedIAudioRenderClient::ReleaseBuffer");
|
||||
});
|
||||
|
||||
if (this->client->backend) {
|
||||
SAFE_CALL("AudioBackend", "on_release_buffer", this->client->backend->on_release_buffer(
|
||||
NumFramesWritten,
|
||||
dwFlags));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
// fix for audio pop effect
|
||||
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
||||
|
||||
// zero out = mute
|
||||
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
||||
|
||||
this->buffers_to_mute--;
|
||||
}
|
||||
|
||||
CHECK_RESULT(pReal->ReleaseBuffer(NumFramesWritten, dwFlags));
|
||||
}
|
||||
38
hooks/audio/backends/wasapi/audio_render_client.h
Normal file
38
hooks/audio/backends/wasapi/audio_render_client.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
struct WrappedIAudioClient;
|
||||
|
||||
// {1CB6ABEE-1181-4FF7-8449-1CA18C2109E3}
|
||||
static const GUID IID_WrappedIAudioRenderClient = {
|
||||
0x1cb6abee, 0x1181, 0x4ff7, { 0x84, 0x49, 0x1c, 0xa1, 0x8c, 0x21, 0x09, 0xe3 }
|
||||
};
|
||||
|
||||
struct WrappedIAudioRenderClient : IAudioRenderClient {
|
||||
explicit WrappedIAudioRenderClient(WrappedIAudioClient *client, IAudioRenderClient *orig) : pReal(orig), client(client) {
|
||||
}
|
||||
|
||||
WrappedIAudioRenderClient(const WrappedIAudioRenderClient &) = delete;
|
||||
WrappedIAudioRenderClient &operator=(const WrappedIAudioRenderClient &) = delete;
|
||||
|
||||
virtual ~WrappedIAudioRenderClient() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE GetBuffer(UINT32 NumFramesRequested, BYTE **ppData) override;
|
||||
HRESULT STDMETHODCALLTYPE ReleaseBuffer(UINT32 NumFramesWritten, DWORD dwFlags) override;
|
||||
#pragma endregion
|
||||
|
||||
IAudioRenderClient *const pReal;
|
||||
WrappedIAudioClient *const client;
|
||||
|
||||
int buffers_to_mute = 16;
|
||||
BYTE *audio_buffer = nullptr;
|
||||
};
|
||||
51
hooks/audio/backends/wasapi/defs.h
Normal file
51
hooks/audio/backends/wasapi/defs.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
|
||||
// missing from MinGW
|
||||
#ifndef AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY
|
||||
#define AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY 0x08000000
|
||||
#endif
|
||||
#ifndef AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
|
||||
#define AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM 0x80000000
|
||||
#endif
|
||||
|
||||
// defined starting with Windows 10, version 1803
|
||||
// https://docs.microsoft.com/en-us/windows/win32/coreaudio/audclnt-streamflags-xxx-constants
|
||||
#ifndef AUDCLNT_STREAMFLAGS_PREVENT_LOOPBACK_CAPTURE
|
||||
#define AUDCLNT_STREAMFLAGS_PREVENT_LOOPBACK_CAPTURE 0x01000000
|
||||
#endif
|
||||
|
||||
DEFINE_GUID(GUID_KSDATAFORMAT_SUBTYPE_PCM,
|
||||
0x00000001, 0x0000, 0x0010,
|
||||
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
|
||||
|
||||
DEFINE_GUID(GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT,
|
||||
0x00000003, 0x0000, 0x0010,
|
||||
0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(IID_IAudioClient,
|
||||
0x1cb9ad4c, 0xdbfa, 0x4c32,
|
||||
0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2);
|
||||
|
||||
DEFINE_GUID(IID_IAudioClock,
|
||||
0xcd63314f, 0x3fba, 0x4a1b,
|
||||
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
|
||||
|
||||
DEFINE_GUID(IID_IAudioClock2,
|
||||
0x6f49ff73, 0x6727, 0x49ac,
|
||||
0xa0, 0x08, 0xd9, 0x8c, 0xf5, 0xe7, 0x00, 0x48);
|
||||
|
||||
DEFINE_GUID(IID_IAudioClockAdjustment,
|
||||
0xf6e4c0a0, 0x46d9, 0x4fb8,
|
||||
0xbe, 0x21, 0x57, 0xa3, 0xef, 0x2b, 0x62, 0x6c);
|
||||
|
||||
DEFINE_GUID(IID_IAudioRenderClient,
|
||||
0xf294acfc, 0x3146, 0x4483,
|
||||
0xa7, 0xbf, 0xad, 0xdc, 0xa7, 0xc2, 0x60, 0xe2);
|
||||
|
||||
DEFINE_GUID(IID_IAudioSessionControl,
|
||||
0xf4b1a599, 0x7266, 0x4319,
|
||||
0xa8, 0xca, 0xe7, 0x0a, 0xcb, 0x11, 0xe8, 0xcd);
|
||||
#endif
|
||||
216
hooks/audio/backends/wasapi/dummy_audio_client.cpp
Normal file
216
hooks/audio/backends/wasapi/dummy_audio_client.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "dummy_audio_client.h"
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/util.h"
|
||||
|
||||
#include "defs.h"
|
||||
#include "dummy_audio_clock.h"
|
||||
#include "dummy_audio_render_client.h"
|
||||
#include "dummy_audio_session_control.h"
|
||||
#include "util.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
#if 0
|
||||
#define WRAP_DEBUG log_misc("audio::wasapi", "{}::{}", CLASS_NAME, __func__)
|
||||
#define WRAP_DEBUG_FMT(format, ...) log_misc("audio::wasapi", format, __VA_ARGS__)
|
||||
#else
|
||||
#define WRAP_DEBUG do {} while (0)
|
||||
#define WRAP_DEBUG_FMT(format, ...) do {} while (0)
|
||||
#endif
|
||||
|
||||
#if 1
|
||||
#define WRAP_VERBOSE log_misc("audio::wasapi", "{}::{}", CLASS_NAME, __func__)
|
||||
#else
|
||||
#define WRAP_VERBOSE do {} while (0)
|
||||
#endif
|
||||
|
||||
const char CLASS_NAME[] = "DummyIAudioClient";
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_DummyIAudioClient ||
|
||||
riid == IID_IAudioClient)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioClient::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioClient::Release() {
|
||||
ULONG refs = --this->ref_cnt;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::Initialize(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
DWORD StreamFlags,
|
||||
REFERENCE_TIME hnsBufferDuration,
|
||||
REFERENCE_TIME hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!pFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
// verbose output
|
||||
log_info("audio::wasapi", "IAudioClient::Initialize hook hit");
|
||||
log_info("audio::wasapi", "... ShareMode : {}", share_mode_str(ShareMode));
|
||||
log_info("audio::wasapi", "... StreamFlags : {}", stream_flags_str(StreamFlags));
|
||||
log_info("audio::wasapi", "... hnsBufferDuration : {}", hnsBufferDuration);
|
||||
log_info("audio::wasapi", "... hnsPeriodicity : {}", hnsPeriodicity);
|
||||
print_format(pFormat);
|
||||
|
||||
CHECK_RESULT(this->backend->on_initialize(
|
||||
&ShareMode,
|
||||
&StreamFlags,
|
||||
&hnsBufferDuration,
|
||||
&hnsPeriodicity,
|
||||
pFormat,
|
||||
AudioSessionGuid));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetBufferSize(UINT32 *pNumBufferFrames) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioClient::GetBufferSize");
|
||||
});
|
||||
|
||||
CHECK_RESULT(this->backend->on_get_buffer_size(pNumBufferFrames));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetStreamLatency(REFERENCE_TIME *phnsLatency) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioClient::GetStreamLatency");
|
||||
});
|
||||
|
||||
CHECK_RESULT(this->backend->on_get_stream_latency(phnsLatency));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetCurrentPadding(UINT32 *pNumPaddingFrames) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioClient::GetCurrentPadding");
|
||||
});
|
||||
|
||||
if (!pNumPaddingFrames) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
std::optional<uint32_t> padding_frames;
|
||||
|
||||
HRESULT ret = this->backend->on_get_current_padding(padding_frames);
|
||||
|
||||
if (SUCCEEDED(ret)) {
|
||||
*pNumPaddingFrames = padding_frames.value_or(0);
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::IsFormatSupported(
|
||||
AUDCLNT_SHAREMODE ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch)
|
||||
{
|
||||
WRAP_VERBOSE;
|
||||
|
||||
if (!pFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
CHECK_RESULT(this->backend->on_is_format_supported(&ShareMode, pFormat, ppClosestMatch));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetMixFormat(WAVEFORMATEX **ppDeviceFormat) {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
if (!ppDeviceFormat) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
CHECK_RESULT(this->backend->on_get_mix_format(ppDeviceFormat));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetDevicePeriod(
|
||||
REFERENCE_TIME *phnsDefaultDevicePeriod,
|
||||
REFERENCE_TIME *phnsMinimumDevicePeriod)
|
||||
{
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioClient::GetDevicePeriod");
|
||||
});
|
||||
|
||||
CHECK_RESULT(this->backend->on_get_device_period(
|
||||
phnsDefaultDevicePeriod,
|
||||
phnsMinimumDevicePeriod));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::Start() {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
HRESULT ret = this->backend->on_start();
|
||||
|
||||
if (SUCCEEDED(ret)) {
|
||||
for (auto &handler : this->session_notification_handlers) {
|
||||
handler->OnStateChanged(AudioSessionStateActive);
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::Stop() {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
HRESULT ret = this->backend->on_stop();
|
||||
|
||||
if (SUCCEEDED(ret)) {
|
||||
for (auto &handler : this->session_notification_handlers) {
|
||||
handler->OnStateChanged(AudioSessionStateInactive);
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(ret);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::Reset() {
|
||||
WRAP_VERBOSE;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::SetEventHandle(HANDLE eventHandle) {
|
||||
WRAP_VERBOSE;
|
||||
CHECK_RESULT(this->backend->on_set_event_handle(&eventHandle));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClient::GetService(REFIID riid, void **ppv) {
|
||||
WRAP_DEBUG_FMT("DummyIAudioClient::GetService({})", guid2s(riid));
|
||||
|
||||
if (ppv) {
|
||||
if (riid == IID_IAudioRenderClient) {
|
||||
*ppv = new DummyIAudioRenderClient(this);
|
||||
|
||||
return S_OK;
|
||||
} else if (riid == IID_IAudioSessionControl) {
|
||||
*ppv = new DummyIAudioSessionControl(this);
|
||||
|
||||
return S_OK;
|
||||
} else if (riid == IID_IAudioClock) {
|
||||
*ppv = new DummyIAudioClock(this->backend);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
CHECK_RESULT(E_NOINTERFACE);
|
||||
}
|
||||
63
hooks/audio/backends/wasapi/dummy_audio_client.h
Normal file
63
hooks/audio/backends/wasapi/dummy_audio_client.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
#include <audiopolicy.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "hooks/audio/implementations/backend.h"
|
||||
#include "hooks/audio/implementations/asio.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
// {F0842A04-0F8E-4F5C-B3FF-0ED24C589BDA}
|
||||
static const GUID IID_DummyIAudioClient = {
|
||||
0xf0842a04, 0x0f8e, 0x4f5c, { 0xb3, 0xff, 0x0e, 0xd2, 0x4c, 0x58, 0x9b, 0xda }
|
||||
};
|
||||
|
||||
struct DummyIAudioClient : IAudioClient {
|
||||
explicit DummyIAudioClient(AudioBackend *backend) : backend(backend) {
|
||||
if (!this->backend) {
|
||||
log_fatal("audio::wasapi", "DummyIAudioClient: no backend initialized");
|
||||
}
|
||||
}
|
||||
|
||||
DummyIAudioClient(const DummyIAudioClient &) = delete;
|
||||
DummyIAudioClient &operator=(const DummyIAudioClient &) = delete;
|
||||
|
||||
virtual ~DummyIAudioClient() {
|
||||
log_misc("audio::wasapi", "~DummyIAudioClient");
|
||||
|
||||
delete this->backend;
|
||||
}
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE Initialize(AUDCLNT_SHAREMODE ShareMode, DWORD StreamFlags, REFERENCE_TIME hnsBufferDuration, REFERENCE_TIME hnsPeriodicity, const WAVEFORMATEX *pFormat, LPCGUID AudioSessionGuid) override;
|
||||
HRESULT STDMETHODCALLTYPE GetBufferSize(UINT32 *pNumBufferFrames) override;
|
||||
HRESULT STDMETHODCALLTYPE GetStreamLatency(REFERENCE_TIME *phnsLatency) override;
|
||||
HRESULT STDMETHODCALLTYPE GetCurrentPadding(UINT32 *pNumPaddingFrames) override;
|
||||
HRESULT STDMETHODCALLTYPE IsFormatSupported(AUDCLNT_SHAREMODE ShareMode, const WAVEFORMATEX *pFormat, WAVEFORMATEX **ppClosestMatch) override;
|
||||
HRESULT STDMETHODCALLTYPE GetMixFormat(WAVEFORMATEX **ppDeviceFormat) override;
|
||||
HRESULT STDMETHODCALLTYPE GetDevicePeriod(REFERENCE_TIME *phnsDefaultDevicePeriod, REFERENCE_TIME *phnsMinimumDevicePeriod) override;
|
||||
HRESULT STDMETHODCALLTYPE Start() override;
|
||||
HRESULT STDMETHODCALLTYPE Stop() override;
|
||||
HRESULT STDMETHODCALLTYPE Reset() override;
|
||||
HRESULT STDMETHODCALLTYPE SetEventHandle(HANDLE eventHandle) override;
|
||||
HRESULT STDMETHODCALLTYPE GetService(REFIID riid, void **ppv) override;
|
||||
#pragma endregion
|
||||
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
AudioBackend *const backend;
|
||||
|
||||
std::vector<IAudioSessionEvents *> session_notification_handlers;
|
||||
};
|
||||
62
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
Normal file
62
hooks/audio/backends/wasapi/dummy_audio_clock.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include "dummy_audio_clock.h"
|
||||
|
||||
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
|
||||
|
||||
#include "wasapi_private.h"
|
||||
|
||||
const char CLASS_NAME[] = "DummyIAudioClock";
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClock::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_DummyIAudioClock ||
|
||||
riid == IID_IAudioClock)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioClock::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioClock::Release() {
|
||||
ULONG refs = --this->ref_cnt;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioClock
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClock::GetFrequency(UINT64 *pu64Frequency) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioClock::GetFrequency");
|
||||
});
|
||||
|
||||
if (!pu64Frequency) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
*pu64Frequency = static_cast<UINT64>(this->backend->format().Format.nSamplesPerSec);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClock::GetPosition(
|
||||
UINT64 *pu64Position,
|
||||
UINT64 *pu64QPCPosition)
|
||||
{
|
||||
CHECK_RESULT(E_NOTIMPL);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioClock::GetCharacteristics(DWORD *pdwCharacteristics) {
|
||||
CHECK_RESULT(E_NOTIMPL);
|
||||
}
|
||||
39
hooks/audio/backends/wasapi/dummy_audio_clock.h
Normal file
39
hooks/audio/backends/wasapi/dummy_audio_clock.h
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audiopolicy.h>
|
||||
|
||||
struct AudioBackend;
|
||||
|
||||
// {8AE52B4A-ACC4-420C-9169-BA8AF07A251F}
|
||||
static const GUID IID_DummyIAudioClock = {
|
||||
0x8ae52b4a, 0xacc4, 0x420c, { 0x91, 0x69, 0xba, 0x8a, 0xf0, 0x7a, 0x25, 0x1f }
|
||||
};
|
||||
|
||||
struct DummyIAudioClock : IAudioClock {
|
||||
explicit DummyIAudioClock(AudioBackend *backend) : backend(backend) {
|
||||
}
|
||||
|
||||
DummyIAudioClock(const DummyIAudioClock &) = delete;
|
||||
DummyIAudioClock &operator=(const DummyIAudioClock &) = delete;
|
||||
|
||||
virtual ~DummyIAudioClock() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioClock
|
||||
HRESULT STDMETHODCALLTYPE GetFrequency(UINT64 *pu64Frequency) override;
|
||||
HRESULT STDMETHODCALLTYPE GetPosition(UINT64 *pu64Position, UINT64 *pu64QPCPosition) override;
|
||||
HRESULT STDMETHODCALLTYPE GetCharacteristics(DWORD *pdwCharacteristics) override;
|
||||
#pragma endregion
|
||||
|
||||
AudioBackend *const backend;
|
||||
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
};
|
||||
57
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
Normal file
57
hooks/audio/backends/wasapi/dummy_audio_render_client.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "dummy_audio_render_client.h"
|
||||
|
||||
#include "dummy_audio_client.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
const char CLASS_NAME[] = "DummyIAudioRenderClient";
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_DummyIAudioRenderClient ||
|
||||
riid == IID_IAudioRenderClient)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioRenderClient::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioRenderClient::Release() {
|
||||
ULONG refs = --this->ref_cnt;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioRenderClient
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioRenderClient::GetBuffer(UINT32 NumFramesRequested, BYTE **ppData) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioRenderClient::GetBuffer");
|
||||
});
|
||||
|
||||
CHECK_RESULT(this->client->backend->on_get_buffer(
|
||||
NumFramesRequested,
|
||||
ppData));
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioRenderClient::ReleaseBuffer(UINT32 NumFramesWritten, DWORD dwFlags) {
|
||||
static std::once_flag printed;
|
||||
std::call_once(printed, []() {
|
||||
log_misc("audio::wasapi", "DummyIAudioRenderClient::ReleaseBuffer");
|
||||
});
|
||||
|
||||
CHECK_RESULT(this->client->backend->on_release_buffer(
|
||||
NumFramesWritten,
|
||||
dwFlags));
|
||||
}
|
||||
38
hooks/audio/backends/wasapi/dummy_audio_render_client.h
Normal file
38
hooks/audio/backends/wasapi/dummy_audio_render_client.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
|
||||
struct DummyIAudioClient;
|
||||
|
||||
// {453BF965-DDA4-4234-8846-F02BA5E874B7}
|
||||
static const GUID IID_DummyIAudioRenderClient = {
|
||||
0x453bf965, 0xdda4, 0x4234, { 0x88, 0x46, 0xf0, 0x2b, 0xa5, 0xe8, 0x74, 0xb7 }
|
||||
};
|
||||
|
||||
struct DummyIAudioRenderClient : IAudioRenderClient {
|
||||
explicit DummyIAudioRenderClient(DummyIAudioClient *client) : client(client) {
|
||||
}
|
||||
|
||||
DummyIAudioRenderClient(const DummyIAudioRenderClient &) = delete;
|
||||
DummyIAudioRenderClient &operator=(const DummyIAudioRenderClient &) = delete;
|
||||
|
||||
virtual ~DummyIAudioRenderClient() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioClient
|
||||
HRESULT STDMETHODCALLTYPE GetBuffer(UINT32 NumFramesRequested, BYTE **ppData) override;
|
||||
HRESULT STDMETHODCALLTYPE ReleaseBuffer(UINT32 NumFramesWritten, DWORD dwFlags) override;
|
||||
#pragma endregion
|
||||
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
DummyIAudioClient *const client;
|
||||
};
|
||||
176
hooks/audio/backends/wasapi/dummy_audio_session_control.cpp
Normal file
176
hooks/audio/backends/wasapi/dummy_audio_session_control.cpp
Normal file
@@ -0,0 +1,176 @@
|
||||
#include "dummy_audio_session_control.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "dummy_audio_client.h"
|
||||
#include "wasapi_private.h"
|
||||
|
||||
#if 1
|
||||
#define WRAP_DEBUG log_misc("audio::wasapi", "{}::{}", CLASS_NAME, __func__)
|
||||
#else
|
||||
#define WRAP_DEBUG do {} while (0)
|
||||
#endif
|
||||
|
||||
const char CLASS_NAME[] = "DummyIAudioSessionControl";
|
||||
|
||||
// IUnknown
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::QueryInterface(REFIID riid, void **ppvObj) {
|
||||
if (ppvObj == nullptr) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
if (riid == IID_DummyIAudioSessionControl ||
|
||||
riid == IID_IAudioSessionControl)
|
||||
{
|
||||
this->AddRef();
|
||||
*ppvObj = this;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioSessionControl::AddRef() {
|
||||
return ++this->ref_cnt;
|
||||
}
|
||||
ULONG STDMETHODCALLTYPE DummyIAudioSessionControl::Release() {
|
||||
ULONG refs = --this->ref_cnt;
|
||||
|
||||
if (refs == 0) {
|
||||
delete this;
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
// IAudioSessionControl
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::GetState(AudioSessionState *pRetVal) {
|
||||
CHECK_RESULT(E_NOTIMPL);
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::GetDisplayName(LPWSTR *pRetVal) {
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!pRetVal) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
auto length = this->display_name.length();
|
||||
auto value = reinterpret_cast<LPWSTR>(CoTaskMemAlloc(length + 1));
|
||||
|
||||
if (!value) {
|
||||
CHECK_RESULT(E_OUTOFMEMORY);
|
||||
}
|
||||
|
||||
memcpy(value, this->display_name.c_str(), length);
|
||||
value[length] = L'\0';
|
||||
|
||||
*pRetVal = value;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::SetDisplayName(
|
||||
LPCWSTR Value,
|
||||
LPCGUID EventContext)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!Value) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
this->display_name = std::wstring(Value);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::GetIconPath(LPWSTR *pRetVal) {
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!pRetVal) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
auto length = this->icon_path.length();
|
||||
auto value = reinterpret_cast<LPWSTR>(CoTaskMemAlloc(length + 1));
|
||||
|
||||
if (!value) {
|
||||
CHECK_RESULT(E_OUTOFMEMORY);
|
||||
}
|
||||
|
||||
memcpy(value, this->icon_path.c_str(), length);
|
||||
value[length] = L'\0';
|
||||
|
||||
*pRetVal = value;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::SetIconPath(
|
||||
LPCWSTR Value,
|
||||
LPCGUID EventContext)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!Value) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
this->icon_path = std::wstring(Value);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::GetGroupingParam(GUID *pRetVal) {
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!pRetVal) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
memcpy(pRetVal, &this->grouping_param, sizeof(this->grouping_param));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::SetGroupingParam(
|
||||
LPCGUID Override,
|
||||
LPCGUID EventContext)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!Override) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
memcpy(&this->grouping_param, Override, sizeof(this->grouping_param));
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::RegisterAudioSessionNotification(
|
||||
IAudioSessionEvents *NewNotifications)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!NewNotifications) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
this->client->session_notification_handlers.emplace_back(NewNotifications);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT STDMETHODCALLTYPE DummyIAudioSessionControl::UnregisterAudioSessionNotification(
|
||||
IAudioSessionEvents *NewNotifications)
|
||||
{
|
||||
WRAP_DEBUG;
|
||||
|
||||
if (!NewNotifications) {
|
||||
return E_POINTER;
|
||||
}
|
||||
|
||||
this->client->session_notification_handlers.erase(
|
||||
std::remove(
|
||||
this->client->session_notification_handlers.begin(),
|
||||
this->client->session_notification_handlers.end(),
|
||||
NewNotifications
|
||||
),
|
||||
this->client->session_notification_handlers.end());
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
50
hooks/audio/backends/wasapi/dummy_audio_session_control.h
Normal file
50
hooks/audio/backends/wasapi/dummy_audio_session_control.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audiopolicy.h>
|
||||
|
||||
struct DummyIAudioClient;
|
||||
|
||||
// {5412A875-C82F-451F-B29A-0E18DB1CDFA2}
|
||||
static const GUID IID_DummyIAudioSessionControl = {
|
||||
0x5412a875, 0xc82f, 0x451f, { 0xb2, 0x9a, 0x0e, 0x18, 0xdb, 0x1c, 0xdf, 0xa2 }
|
||||
};
|
||||
|
||||
struct DummyIAudioSessionControl : IAudioSessionControl {
|
||||
explicit DummyIAudioSessionControl(DummyIAudioClient *client) : client(client) {
|
||||
}
|
||||
|
||||
DummyIAudioSessionControl(const DummyIAudioSessionControl &) = delete;
|
||||
DummyIAudioSessionControl &operator=(const DummyIAudioSessionControl &) = delete;
|
||||
|
||||
virtual ~DummyIAudioSessionControl() = default;
|
||||
|
||||
#pragma region IUnknown
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioSessionControl
|
||||
HRESULT STDMETHODCALLTYPE GetState(AudioSessionState *pRetVal) override;
|
||||
HRESULT STDMETHODCALLTYPE GetDisplayName(LPWSTR *pRetVal) override;
|
||||
HRESULT STDMETHODCALLTYPE SetDisplayName(LPCWSTR Value, LPCGUID EventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetIconPath(LPWSTR *pRetVal) override;
|
||||
HRESULT STDMETHODCALLTYPE SetIconPath(LPCWSTR Value, LPCGUID EventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE GetGroupingParam(GUID *pRetVal) override;
|
||||
HRESULT STDMETHODCALLTYPE SetGroupingParam(LPCGUID Override, LPCGUID EventContext) override;
|
||||
HRESULT STDMETHODCALLTYPE RegisterAudioSessionNotification(IAudioSessionEvents *NewNotifications) override;
|
||||
HRESULT STDMETHODCALLTYPE UnregisterAudioSessionNotification(IAudioSessionEvents *NewNotifications) override;
|
||||
#pragma endregion
|
||||
|
||||
DummyIAudioClient *const client;
|
||||
|
||||
std::atomic<ULONG> ref_cnt = 1;
|
||||
|
||||
std::wstring display_name = L"Dummy Audio Device";
|
||||
std::wstring icon_path = L"";
|
||||
GUID grouping_param = GUID_NULL;
|
||||
};
|
||||
153
hooks/audio/backends/wasapi/low_latency_client.cpp
Normal file
153
hooks/audio/backends/wasapi/low_latency_client.cpp
Normal file
@@ -0,0 +1,153 @@
|
||||
#include "low_latency_client.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#ifdef _MSC_VER
|
||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||
0xBCDE0395, 0xE52F, 0x467C,
|
||||
0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E);
|
||||
#endif
|
||||
|
||||
#define PRINT_FAILED_RESULT(name, ret) \
|
||||
log_warning("audio::lowlatency", "{} failed, hr={}", name, FMT_HRESULT(ret))
|
||||
|
||||
namespace hooks::audio {
|
||||
bool LOW_LATENCY_SHARED_WASAPI = false;
|
||||
static bool COM_INITIALIZED = false;
|
||||
static LowLatencyAudioClient *LOW_LATENCY_CLIENT = nullptr;
|
||||
|
||||
void init_low_latency() {
|
||||
if (!LOW_LATENCY_SHARED_WASAPI) {
|
||||
return;
|
||||
}
|
||||
log_info("audio::lowlatency", "initializing");
|
||||
|
||||
HRESULT hr;
|
||||
|
||||
// initialize COM
|
||||
COM_INITIALIZED = true;
|
||||
hr = CoInitialize(NULL);
|
||||
if (FAILED(hr)) {
|
||||
PRINT_FAILED_RESULT("CoInitialize", hr);
|
||||
return;
|
||||
}
|
||||
|
||||
// initialize device enumerator
|
||||
IMMDeviceEnumerator* enumerator;
|
||||
hr = CoCreateInstance(
|
||||
CLSID_MMDeviceEnumerator,
|
||||
NULL,
|
||||
CLSCTX_ALL,
|
||||
IID_IMMDeviceEnumerator,
|
||||
reinterpret_cast<void**>(&enumerator));
|
||||
if (FAILED(hr)) {
|
||||
PRINT_FAILED_RESULT("CoCreateInstance(CLSID_MMDeviceEnumerator)", hr);
|
||||
return;
|
||||
}
|
||||
|
||||
// get default audio endpoint from enumerator
|
||||
IMMDevice* device;
|
||||
hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device);
|
||||
if (FAILED(hr)) {
|
||||
PRINT_FAILED_RESULT("GetDefaultAudioEndpoint", hr);
|
||||
return;
|
||||
}
|
||||
enumerator->Release();
|
||||
|
||||
// start the client using the default audio endpoint
|
||||
LOW_LATENCY_CLIENT = hooks::audio::LowLatencyAudioClient::Create(device);
|
||||
log_info("audio::lowlatency", "initialized");
|
||||
}
|
||||
|
||||
void stop_low_latency() {
|
||||
if (!LOW_LATENCY_SHARED_WASAPI) {
|
||||
return;
|
||||
}
|
||||
log_info("audio::lowlatency", "stopping");
|
||||
if (LOW_LATENCY_CLIENT) {
|
||||
delete LOW_LATENCY_CLIENT;
|
||||
LOW_LATENCY_CLIENT = nullptr;
|
||||
}
|
||||
if (COM_INITIALIZED) {
|
||||
COM_INITIALIZED = false;
|
||||
CoUninitialize();
|
||||
}
|
||||
log_info("audio::lowlatency", "stopped");
|
||||
}
|
||||
}
|
||||
|
||||
using namespace hooks::audio;
|
||||
|
||||
LowLatencyAudioClient::LowLatencyAudioClient(IAudioClient3* audioClient) : audioClient(audioClient) {}
|
||||
|
||||
LowLatencyAudioClient::~LowLatencyAudioClient() {
|
||||
if (this->audioClient) {
|
||||
HRESULT ret;
|
||||
ret = this->audioClient->Stop();
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient3::Stop", ret);
|
||||
}
|
||||
this->audioClient->Release();
|
||||
this->audioClient = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
LowLatencyAudioClient *LowLatencyAudioClient::Create(IMMDevice *device) {
|
||||
HRESULT ret;
|
||||
UINT32 minPeriod;
|
||||
UINT32 defaultPeriod;
|
||||
UINT32 fundamentalPeriod;
|
||||
UINT32 maxPeriod;
|
||||
PWAVEFORMATEX pFormat;
|
||||
IAudioClient3* audioClient;
|
||||
|
||||
ret = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, NULL, reinterpret_cast<void**>(&audioClient));
|
||||
device->Release();
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IMMDevice::Activate(IID_IAudioClient3...)", ret);
|
||||
log_warning("audio::lowlatency", "note that only Windows 10 and above supports IAudioClient3");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = audioClient->GetMixFormat(&pFormat);
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient3::GetMixFormat", ret);
|
||||
audioClient->Release();
|
||||
audioClient = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = audioClient->GetSharedModeEnginePeriod(pFormat, &defaultPeriod, &fundamentalPeriod, &minPeriod, &maxPeriod);
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient3::GetSharedModeEnginePeriod", ret);
|
||||
audioClient->Release();
|
||||
audioClient = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = audioClient->InitializeSharedAudioStream(0, minPeriod, pFormat, NULL);
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient3::InitializeSharedAudioStream", ret);
|
||||
audioClient->Release();
|
||||
audioClient = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ret = audioClient->Start();
|
||||
if (FAILED(ret)) {
|
||||
PRINT_FAILED_RESULT("IAudioClient3::Start", ret);
|
||||
audioClient->Release();
|
||||
audioClient = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
log_info("audio::lowlatency", "low latency shared mode audio client initialized successfully.");
|
||||
log_info("audio::lowlatency", "this is NOT used to output sound...");
|
||||
log_info("audio::lowlatency", "but rather to reduce buffer sizes when the game requests an audio client at a later point");
|
||||
log_info("audio::lowlatency", "has no effect if the game uses exclusive WASAPI or ASIO!");
|
||||
log_info("audio::lowlatency", "... sample rate : {} Hz", pFormat->nSamplesPerSec);
|
||||
log_info("audio::lowlatency", "... min buffer size : {} samples ({} ms)", minPeriod, 1000.0f * minPeriod / pFormat->nSamplesPerSec);
|
||||
log_info("audio::lowlatency", "... max buffer size : {} samples ({} ms)", maxPeriod, 1000.0f * maxPeriod / pFormat->nSamplesPerSec);
|
||||
log_info("audio::lowlatency", "... default buffer size : {} samples ({} ms)", defaultPeriod, 1000.0f * defaultPeriod / pFormat->nSamplesPerSec);
|
||||
log_info("audio::lowlatency", "... Windows will use minimum buffer size (instead of default) for shared mode audio clients from now on");
|
||||
return new LowLatencyAudioClient(audioClient);
|
||||
}
|
||||
22
hooks/audio/backends/wasapi/low_latency_client.h
Normal file
22
hooks/audio/backends/wasapi/low_latency_client.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <initguid.h>
|
||||
#include <audioclient.h>
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
|
||||
namespace hooks::audio {
|
||||
void init_low_latency();
|
||||
void stop_low_latency();
|
||||
|
||||
class LowLatencyAudioClient {
|
||||
public:
|
||||
static LowLatencyAudioClient *Create(IMMDevice *device);
|
||||
~LowLatencyAudioClient();
|
||||
|
||||
private:
|
||||
LowLatencyAudioClient(IAudioClient3* audioClient);
|
||||
IAudioClient3* audioClient;
|
||||
};
|
||||
}
|
||||
20
hooks/audio/backends/wasapi/util.cpp
Normal file
20
hooks/audio/backends/wasapi/util.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "util.h"
|
||||
|
||||
#include <audioclient.h>
|
||||
|
||||
#include "util/flags_helper.h"
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
std::string stream_flags_str(DWORD flags) {
|
||||
FLAGS_START(flags);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_CROSSPROCESS);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_LOOPBACK);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_EVENTCALLBACK);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_NOPERSIST);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_RATEADJUST);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_PREVENT_LOOPBACK_CAPTURE);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM);
|
||||
FLAG(flags, AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY);
|
||||
FLAGS_END(flags);
|
||||
}
|
||||
8
hooks/audio/backends/wasapi/util.h
Normal file
8
hooks/audio/backends/wasapi/util.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
#include <mmreg.h>
|
||||
|
||||
std::string stream_flags_str(DWORD flags);
|
||||
29
hooks/audio/backends/wasapi/wasapi_private.h
Normal file
29
hooks/audio/backends/wasapi/wasapi_private.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
#define PRINT_FAILED_RESULT(class_name, func_name, ret) \
|
||||
if (AUDIO_LOG_HRESULT) { \
|
||||
log_warning("audio::wasapi", "{}::{} failed, hr={}", class_name, func_name, FMT_HRESULT(ret)); \
|
||||
}
|
||||
|
||||
#define SAFE_CALL(class_name, func_name, x) \
|
||||
do { \
|
||||
HRESULT __hr = (x); \
|
||||
if (FAILED(__hr)) { \
|
||||
PRINT_FAILED_RESULT(class_name, func_name, __hr); \
|
||||
return __hr; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_RESULT(x) \
|
||||
do { \
|
||||
HRESULT __ret = (x); \
|
||||
if (FAILED(__ret)) { \
|
||||
PRINT_FAILED_RESULT(CLASS_NAME, __func__, __ret); \
|
||||
} \
|
||||
return __ret; \
|
||||
} while (0)
|
||||
99
hooks/audio/buffer.cpp
Normal file
99
hooks/audio/buffer.cpp
Normal file
@@ -0,0 +1,99 @@
|
||||
#include "buffer.h"
|
||||
|
||||
void convert_sample_type(
|
||||
const size_t channels,
|
||||
uint8_t *buffer,
|
||||
const size_t source_size,
|
||||
std::vector<double> &temp_buffer,
|
||||
const SampleType source_type,
|
||||
const SampleType dest_type)
|
||||
{
|
||||
// fast case: same type
|
||||
if (source_type == dest_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t source_sample_size = sample_type_size(source_type);
|
||||
|
||||
// number of samples *per channel*
|
||||
const size_t num_samples = source_size / source_sample_size / channels;
|
||||
|
||||
// calculate the required size for the temporary buffer
|
||||
// samples are converted to doubles and then converted to the target sample type
|
||||
const size_t temp_size = num_samples * channels;
|
||||
|
||||
// resize temporary buffer if needed
|
||||
if (temp_buffer.size() < temp_size) {
|
||||
temp_buffer.resize(temp_size);
|
||||
}
|
||||
|
||||
#define SAMPLE_LOOP(VAR) for (size_t VAR = 0; VAR < num_samples * channels; VAR++)
|
||||
|
||||
// converts to double in temporary buffer
|
||||
#define CONVERT_LOOP(TY) \
|
||||
do { \
|
||||
const auto source = reinterpret_cast<TY *>(buffer); \
|
||||
SAMPLE_LOOP(i) { \
|
||||
temp_buffer[i] = convert_number_to_double<TY>(source[i]); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// converts double back to desired format
|
||||
#define STORE_LOOP(TY) \
|
||||
do { \
|
||||
const auto dest = reinterpret_cast<TY *>(buffer); \
|
||||
SAMPLE_LOOP(i) { \
|
||||
dest[i] = convert_double_to_number<TY>(temp_buffer[i]); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// converts double to float
|
||||
#define STORE_LOOP_FLOAT() \
|
||||
do { \
|
||||
const auto dest = reinterpret_cast<float *>(buffer); \
|
||||
SAMPLE_LOOP(i) { \
|
||||
dest[i] = static_cast<float>(temp_buffer[i]); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
if (source_type == SampleType::SINT_16) {
|
||||
CONVERT_LOOP(int16_t);
|
||||
} else if (source_type == SampleType::SINT_24) {
|
||||
const auto source = reinterpret_cast<int24_t *>(buffer);
|
||||
|
||||
SAMPLE_LOOP(i) {
|
||||
temp_buffer[i] = convert_number_to_double<int32_t, int24_t>(source[i].as_int());
|
||||
}
|
||||
} else if (source_type == SampleType::SINT_32) {
|
||||
CONVERT_LOOP(int32_t);
|
||||
} else if (source_type == SampleType::FLOAT_32) {
|
||||
const auto source = reinterpret_cast<float *>(buffer);
|
||||
|
||||
SAMPLE_LOOP(i) {
|
||||
temp_buffer[i] = source[i];
|
||||
}
|
||||
} else if (source_type == SampleType::FLOAT_64) {
|
||||
memcpy(temp_buffer.data(), buffer, temp_size);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dest_type == SampleType::SINT_16) {
|
||||
STORE_LOOP(int16_t);
|
||||
} else if (dest_type == SampleType::SINT_24) {
|
||||
STORE_LOOP(int24_t);
|
||||
} else if (dest_type == SampleType::SINT_32) {
|
||||
STORE_LOOP(int32_t);
|
||||
} else if (dest_type == SampleType::FLOAT_32) {
|
||||
STORE_LOOP_FLOAT();
|
||||
} else if (dest_type == SampleType::FLOAT_64) {
|
||||
memcpy(buffer, temp_buffer.data(), temp_size);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
#undef STORE_LOOP_FLOAT
|
||||
#undef STORE_LOOP
|
||||
#undef CONVERT_LOOP
|
||||
#undef SAMPLE_LOOP
|
||||
}
|
||||
162
hooks/audio/buffer.h
Normal file
162
hooks/audio/buffer.h
Normal file
@@ -0,0 +1,162 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
#endif
|
||||
#ifdef max
|
||||
#undef max
|
||||
#endif
|
||||
|
||||
enum class SampleType {
|
||||
UNSUPPORTED = 0,
|
||||
SINT_16,
|
||||
SINT_24,
|
||||
SINT_32,
|
||||
FLOAT_32,
|
||||
FLOAT_64,
|
||||
};
|
||||
|
||||
static constexpr const char *sample_type_str(SampleType sample_type) {
|
||||
switch (sample_type) {
|
||||
case SampleType::UNSUPPORTED:
|
||||
return "UNSUPPORTED";
|
||||
case SampleType::SINT_16:
|
||||
return "SINT_16";
|
||||
case SampleType::SINT_24:
|
||||
return "SINT_24";
|
||||
case SampleType::SINT_32:
|
||||
return "SINT_32";
|
||||
case SampleType::FLOAT_32:
|
||||
return "FLOAT_32";
|
||||
case SampleType::FLOAT_64:
|
||||
return "FLOAT_64";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
struct int24_t {
|
||||
uint8_t c3[3] {};
|
||||
|
||||
constexpr int24_t() = default;
|
||||
|
||||
constexpr int24_t(const short &i) noexcept {
|
||||
*this = static_cast<int>(i);
|
||||
}
|
||||
constexpr int24_t(const int &i) noexcept {
|
||||
*this = i;
|
||||
}
|
||||
constexpr int24_t(const long &l) noexcept {
|
||||
*this = static_cast<int>(l);
|
||||
}
|
||||
constexpr int24_t(const float &f) noexcept {
|
||||
*this = static_cast<int>(f);
|
||||
}
|
||||
constexpr int24_t(const double &d) noexcept {
|
||||
*this = static_cast<int>(d);
|
||||
}
|
||||
|
||||
constexpr int24_t &operator=(const int &i) noexcept {
|
||||
c3[0] = (i & 0x0000ffu);
|
||||
c3[1] = (i & 0x00ff00u) >> 8;
|
||||
c3[2] = (i & 0xff0000u) >> 16;
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
constexpr int32_t as_int() noexcept {
|
||||
uint32_t i = c3[0] | (c3[1] << 8) | (c3[2] << 16);
|
||||
if (i & 0x800000) {
|
||||
i |= ~0xffffffu;
|
||||
}
|
||||
return static_cast<int32_t>(i);
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
class std::numeric_limits<int24_t> {
|
||||
public:
|
||||
static constexpr int32_t(min)() noexcept {
|
||||
return -0x800000;
|
||||
}
|
||||
static constexpr int32_t(max)() noexcept {
|
||||
return 0x7fffff;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class conversion_limits {
|
||||
public:
|
||||
static constexpr double absolute_max_value() noexcept {
|
||||
|
||||
// 1.0 is added here because the minimum value is `abs(min)` because of two's complement
|
||||
return static_cast<double>(std::numeric_limits<T>::max()) + 1.0;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(std::numeric_limits<int16_t>::max() == 32767);
|
||||
static_assert(std::numeric_limits<int24_t>::max() == 8388607);
|
||||
static_assert(std::numeric_limits<int32_t>::max() == 2147483647);
|
||||
static_assert(std::numeric_limits<int64_t>::max() == 9223372036854775807LL);
|
||||
static_assert(conversion_limits<int16_t>::absolute_max_value() == 32768.0);
|
||||
static_assert(conversion_limits<int24_t>::absolute_max_value() == 8388608.0);
|
||||
static_assert(conversion_limits<int32_t>::absolute_max_value() == 2147483648.0);
|
||||
static_assert(conversion_limits<int64_t>::absolute_max_value() == 9223372036854775808.0);
|
||||
|
||||
template<typename T, typename U = T>
|
||||
constexpr double convert_number_to_double(T num) {
|
||||
return static_cast<double>(num) / conversion_limits<U>::absolute_max_value();
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr T convert_double_to_number(double num) {
|
||||
constexpr auto ABSOLUTE_MAX_VALUE = conversion_limits<T>::absolute_max_value();
|
||||
constexpr auto MAX_VALUE = static_cast<long>(std::numeric_limits<T>::max());
|
||||
|
||||
return static_cast<T>(std::min(std::lround(num * ABSOLUTE_MAX_VALUE), MAX_VALUE));
|
||||
}
|
||||
|
||||
// ...before Felix makes this mistake again, make sure 24-bit ints are converted with the correct range
|
||||
static_assert(convert_number_to_double<int32_t, int24_t>(8388607) == (8388607.0 / 8388608.0));
|
||||
|
||||
static constexpr size_t sample_type_size(SampleType sample_type) {
|
||||
switch (sample_type) {
|
||||
case SampleType::UNSUPPORTED:
|
||||
return 0;
|
||||
case SampleType::SINT_16:
|
||||
return sizeof(int16_t);
|
||||
case SampleType::SINT_24:
|
||||
return sizeof(int24_t);
|
||||
case SampleType::SINT_32:
|
||||
return sizeof(int32_t);
|
||||
case SampleType::FLOAT_32:
|
||||
return sizeof(float);
|
||||
case SampleType::FLOAT_64:
|
||||
return sizeof(double);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr size_t required_buffer_size(
|
||||
const size_t num_frames,
|
||||
const size_t channels,
|
||||
const SampleType dest_sample_type)
|
||||
{
|
||||
return num_frames * channels * sample_type_size(dest_sample_type);
|
||||
}
|
||||
|
||||
void convert_sample_type(
|
||||
const size_t channels,
|
||||
uint8_t *buffer,
|
||||
const size_t source_size,
|
||||
std::vector<double> &temp_buffer,
|
||||
const SampleType source_type,
|
||||
const SampleType dest_type);
|
||||
1068
hooks/audio/implementations/asio.cpp
Normal file
1068
hooks/audio/implementations/asio.cpp
Normal file
File diff suppressed because it is too large
Load Diff
153
hooks/audio/implementations/asio.h
Normal file
153
hooks/audio/implementations/asio.h
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "external/asio/asio.h"
|
||||
#include "external/asio/iasiodrv.h"
|
||||
#include "external/readerwriterqueue/readerwriterqueue.h"
|
||||
#include "hooks/audio/audio_private.h"
|
||||
#include "hooks/audio/buffer.h"
|
||||
|
||||
#include "backend.h"
|
||||
|
||||
struct AsioBackend;
|
||||
|
||||
extern AsioBackend *ASIO_BACKEND;
|
||||
|
||||
struct BufferEntry {
|
||||
BYTE *buffer;
|
||||
size_t length;
|
||||
size_t read;
|
||||
};
|
||||
|
||||
struct AsioInstanceInfo {
|
||||
long inputs = 0;
|
||||
long outputs = 0;
|
||||
long buffer_min_size = 0;
|
||||
long buffer_max_size = 0;
|
||||
long buffer_preferred_size = 0;
|
||||
long buffer_granularity = 0;
|
||||
long input_latency = 0;
|
||||
long output_latency = 0;
|
||||
};
|
||||
|
||||
struct AsioBackend final : AudioBackend {
|
||||
public:
|
||||
explicit AsioBackend();
|
||||
|
||||
~AsioBackend() final;
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||
|
||||
HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
DWORD *StreamFlags,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid) noexcept override;
|
||||
|
||||
HRESULT on_get_buffer_size(uint32_t *buffer_frames) noexcept override;
|
||||
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) noexcept override;
|
||||
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept override;
|
||||
|
||||
HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch) noexcept override;
|
||||
|
||||
HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept override;
|
||||
|
||||
HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) noexcept override;
|
||||
|
||||
HRESULT on_start() noexcept override;
|
||||
HRESULT on_stop() noexcept override;
|
||||
HRESULT on_set_event_handle(HANDLE *event_handle) noexcept override;
|
||||
|
||||
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **pp_data) noexcept override;
|
||||
HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) noexcept override;
|
||||
|
||||
// for overlay
|
||||
inline const AsioDriverInfo &driver_info() const noexcept {
|
||||
return this->driver_info_;
|
||||
}
|
||||
inline const std::vector<AsioChannelInfo> &channel_info() const noexcept {
|
||||
return this->asio_channel_info_;
|
||||
}
|
||||
inline const AsioInstanceInfo &asio_info() const noexcept {
|
||||
return this->asio_info_;
|
||||
}
|
||||
void open_control_panel();
|
||||
|
||||
std::atomic<uint32_t> queued_frames = 0;
|
||||
std::atomic<size_t> queued_bytes = 0;
|
||||
|
||||
private:
|
||||
using AsioFunction = std::function<AsioError()>;
|
||||
|
||||
enum class AsioThreadState {
|
||||
Closed,
|
||||
Failed,
|
||||
Running,
|
||||
ShuttingDown,
|
||||
};
|
||||
struct AsioThreadMessage {
|
||||
AsioFunction fn;
|
||||
bool result_needed;
|
||||
};
|
||||
|
||||
void set_thread_state(AsioThreadState state);
|
||||
bool load_driver();
|
||||
bool update_driver_info();
|
||||
bool update_latency();
|
||||
bool set_initial_format(WAVEFORMATEXTENSIBLE &target);
|
||||
bool init();
|
||||
bool unload_driver();
|
||||
void reset();
|
||||
AsioError run_on_asio_thread(AsioFunction fn, bool result_needed = true);
|
||||
|
||||
// ASIO callbacks
|
||||
static void buffer_switch(long double_buffer_index, AsioBool direct_process);
|
||||
static void sample_rate_did_change(AsioSampleRate sample_rate);
|
||||
static long asio_message(long selector, long value, void *message, double *opt);
|
||||
|
||||
// helper methods
|
||||
static bool is_supported_subformat(const WAVEFORMATEXTENSIBLE &format_ex) noexcept;
|
||||
REFERENCE_TIME compute_ref_time() const;
|
||||
REFERENCE_TIME compute_latency_ref_time() const;
|
||||
|
||||
std::thread asio_thread;
|
||||
std::atomic_bool asio_thread_initialized = false;
|
||||
std::mutex asio_thread_state_lock;
|
||||
// TODO: use `std::atomic<T>::wait` when stabilized in MSVC
|
||||
std::condition_variable asio_thread_state_cv;
|
||||
std::atomic<AsioThreadState> asio_thread_state = AsioThreadState::Closed;
|
||||
moodycamel::BlockingReaderWriterQueue<AsioThreadMessage> asio_msg_queue_func;
|
||||
moodycamel::BlockingReaderWriterQueue<AsioError> asio_msg_queue_result;
|
||||
|
||||
moodycamel::ReaderWriterQueue<BufferEntry> queue;
|
||||
std::optional<HANDLE> relay_handle = std::nullopt;
|
||||
|
||||
IAsio *asio_driver = nullptr;
|
||||
AsioCallbacks asio_callbacks {};
|
||||
AsioDriverInfo driver_info_ {};
|
||||
AsioInstanceInfo asio_info_;
|
||||
std::vector<AsioChannelInfo> asio_channel_info_;
|
||||
std::vector<AsioBufferInfo> asio_buffers;
|
||||
SampleType asio_sample_type = SampleType::UNSUPPORTED;
|
||||
|
||||
std::atomic_bool started = false;
|
||||
WAVEFORMATEXTENSIBLE format_ {};
|
||||
WAVEFORMATEXTENSIBLE last_checked_format {};
|
||||
|
||||
//std::vector<BYTE> last_sound_buffer;
|
||||
std::vector<double> conversion_sound_buffer;
|
||||
BYTE *active_sound_buffer = nullptr;
|
||||
};
|
||||
55
hooks/audio/implementations/backend.h
Normal file
55
hooks/audio/implementations/backend.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
#include <windows.h>
|
||||
#include <audioclient.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
struct WrappedIAudioClient;
|
||||
|
||||
struct AudioBackend {
|
||||
public:
|
||||
virtual ~AudioBackend() = default;
|
||||
|
||||
[[nodiscard]] virtual const WAVEFORMATEXTENSIBLE &format() const noexcept = 0;
|
||||
|
||||
#pragma region IAudioClient
|
||||
virtual HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
DWORD *StreamFlags,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid) = 0;
|
||||
|
||||
virtual HRESULT on_get_buffer_size(uint32_t *buffer_frames) = 0;
|
||||
|
||||
virtual HRESULT on_get_stream_latency(REFERENCE_TIME *latency) = 0;
|
||||
|
||||
virtual HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) = 0;
|
||||
|
||||
virtual HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch) = 0;
|
||||
|
||||
virtual HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) = 0;
|
||||
|
||||
virtual HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) = 0;
|
||||
|
||||
virtual HRESULT on_start() = 0;
|
||||
|
||||
virtual HRESULT on_stop() = 0;
|
||||
|
||||
virtual HRESULT on_set_event_handle(HANDLE *event_handle) = 0;
|
||||
#pragma endregion
|
||||
|
||||
#pragma region IAudioRenderClient
|
||||
virtual HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) = 0;
|
||||
virtual HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) = 0;
|
||||
#pragma endregion
|
||||
};
|
||||
224
hooks/audio/implementations/wave_out.cpp
Normal file
224
hooks/audio/implementations/wave_out.cpp
Normal file
@@ -0,0 +1,224 @@
|
||||
#include "wave_out.h"
|
||||
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/backends/wasapi/audio_client.h"
|
||||
#include "hooks/audio/backends/wasapi/defs.h"
|
||||
|
||||
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
|
||||
|
||||
HRESULT WaveOutBackend::init(uint32_t buffer_size) {
|
||||
auto &format = hooks::audio::FORMAT.Format;
|
||||
format.wFormatTag = WAVE_FORMAT_PCM;
|
||||
|
||||
log_info("audio::wave_out", "initializing waveOut backend with {} channels, {} Hz, {}-bit",
|
||||
format.nChannels,
|
||||
format.nSamplesPerSec,
|
||||
format.wBitsPerSample);
|
||||
log_info("audio::wave_out", "... nBlockAlign : {} bytes", format.nBlockAlign);
|
||||
log_info("audio::wave_out", "... nAvgBytesPerSec : {} bytes", format.nAvgBytesPerSec);
|
||||
log_info("audio::wave_out", "... buffer reftime : {} ms", WASAPI_TARGET_REFTIME / 10000.f);
|
||||
log_info("audio::wave_out", "... buffer count : {} buffers", _countof(this->hdrs));
|
||||
|
||||
MMRESULT ret = waveOutOpen(
|
||||
&this->handle,
|
||||
WAVE_MAPPER,
|
||||
reinterpret_cast<const WAVEFORMATEX *>(&hooks::audio::FORMAT.Format),
|
||||
reinterpret_cast<DWORD_PTR>(this->dispatcher_event),
|
||||
reinterpret_cast<DWORD_PTR>(nullptr),
|
||||
CALLBACK_EVENT);
|
||||
|
||||
if (ret != MMSYSERR_NOERROR) {
|
||||
log_warning("audio::wave_out", "failed to initialize waveOut backend, hr={:#08x}",
|
||||
static_cast<unsigned>(ret));
|
||||
|
||||
return static_cast<HRESULT>(ret);
|
||||
}
|
||||
|
||||
// initialize buffers
|
||||
for (auto &hdr : this->hdrs) {
|
||||
memset(&hdr, 0, sizeof(hdr));
|
||||
hdr.lpData = new char[buffer_size] {};
|
||||
hdr.dwBufferLength = buffer_size;
|
||||
hdr.dwBytesRecorded = 0;
|
||||
hdr.dwUser = 0;
|
||||
hdr.dwFlags = 0;
|
||||
hdr.dwLoops = 0;
|
||||
hdr.lpNext = nullptr;
|
||||
ret = waveOutPrepareHeader(this->handle, &hdr, sizeof(hdr));
|
||||
|
||||
if (ret != MMSYSERR_NOERROR) {
|
||||
log_warning("audio::wave_out", "failed to prepare waveOut header, hr=0x{:08x}",
|
||||
static_cast<unsigned>(ret));
|
||||
|
||||
return static_cast<HRESULT>(ret);
|
||||
}
|
||||
|
||||
ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
|
||||
|
||||
if (ret != MMSYSERR_NOERROR) {
|
||||
log_warning("audio::wave_out", "failed to write waveOut header, hr=0x{:08x}",
|
||||
static_cast<unsigned>(ret));
|
||||
|
||||
return static_cast<HRESULT>(ret);
|
||||
}
|
||||
}
|
||||
|
||||
// mark as initialized
|
||||
this->initialized = true;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &WaveOutBackend::format() const noexcept {
|
||||
return hooks::audio::FORMAT;
|
||||
}
|
||||
|
||||
HRESULT WaveOutBackend::on_initialize(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
DWORD *StreamFlags,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid) noexcept
|
||||
{
|
||||
*ShareMode = AUDCLNT_SHAREMODE_SHARED;
|
||||
*StreamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
|
||||
AUDCLNT_STREAMFLAGS_RATEADJUST |
|
||||
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
|
||||
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
|
||||
*hnsBufferDuration = WASAPI_TARGET_REFTIME;
|
||||
*hnsPeriodicity = WASAPI_TARGET_REFTIME;
|
||||
|
||||
// this backend only supports stereo audio
|
||||
if (pFormat->nChannels > 2) {
|
||||
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept {
|
||||
*buffer_frames = _countof(this->hdrs);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept {
|
||||
*latency = WASAPI_TARGET_REFTIME;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept {
|
||||
size_t queued_bytes = 0;
|
||||
|
||||
for (auto &hdr : this->hdrs) {
|
||||
if (hdr.dwFlags & WHDR_DONE) {
|
||||
queued_bytes += static_cast<unsigned>(hdr.dwBufferLength);
|
||||
}
|
||||
}
|
||||
|
||||
auto frames = static_cast<uint32_t>(queued_bytes / hooks::audio::FORMAT.Format.nBlockAlign);
|
||||
//log_info("audio::wave_out", "queued_bytes = {}, frames = {}", queued_bytes, frames);
|
||||
|
||||
padding_frames = frames;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch) noexcept
|
||||
{
|
||||
// always support 44.1 kHz, stereo, 16-bits per channel with custom backends
|
||||
if (*ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE &&
|
||||
pFormat->nChannels == 2 &&
|
||||
pFormat->nSamplesPerSec == 44100 &&
|
||||
pFormat->wBitsPerSample == 16)
|
||||
{
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
return AUDCLNT_E_UNSUPPORTED_FORMAT;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept {
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period)
|
||||
{
|
||||
*default_device_period = WASAPI_TARGET_REFTIME;
|
||||
*minimum_device_period = WASAPI_TARGET_REFTIME;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_start() noexcept {
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_stop() noexcept {
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_set_event_handle(HANDLE *event_handle) {
|
||||
this->relay_event = *event_handle;
|
||||
this->dispatcher_event = CreateEvent(nullptr, true, false, nullptr);
|
||||
|
||||
*event_handle = this->dispatcher_event;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
|
||||
auto buffer_size = hooks::audio::FORMAT.Format.nBlockAlign * num_frames_requested;
|
||||
|
||||
if (!this->initialized) {
|
||||
this->init(buffer_size);
|
||||
}
|
||||
|
||||
// wait for a free slot
|
||||
WaitForSingleObject(this->dispatcher_event, INFINITE);
|
||||
|
||||
// allocate temporary sound buffer
|
||||
this->active_sound_buffer = reinterpret_cast<BYTE *>(CoTaskMemAlloc(buffer_size));
|
||||
|
||||
// hand the buffer to the callee
|
||||
*ppData = this->active_sound_buffer;
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
|
||||
bool written = false;
|
||||
|
||||
// reset the dispatcher event
|
||||
ResetEvent(this->dispatcher_event);
|
||||
|
||||
while (!written) {
|
||||
for (WAVEHDR &hdr : this->hdrs) {
|
||||
if (hdr.dwFlags & WHDR_DONE) {
|
||||
memcpy(hdr.lpData, this->active_sound_buffer, hdr.dwBufferLength);
|
||||
|
||||
// write the data to the device now
|
||||
MMRESULT ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
|
||||
|
||||
if (ret != MMSYSERR_NOERROR) {
|
||||
log_warning("audio::wave_out", "failed to write waveOut data, hr={:#08x}",
|
||||
static_cast<unsigned>(ret));
|
||||
}
|
||||
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// avoid pegging the CPU
|
||||
if (!written) {
|
||||
Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// free temporary sound buffer
|
||||
CoTaskMemFree(this->active_sound_buffer);
|
||||
this->active_sound_buffer = nullptr;
|
||||
|
||||
// trigger game audio callback
|
||||
SetEvent(this->relay_event);
|
||||
|
||||
return S_OK;
|
||||
}
|
||||
58
hooks/audio/implementations/wave_out.h
Normal file
58
hooks/audio/implementations/wave_out.h
Normal file
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <mmdeviceapi.h>
|
||||
#include <mmsystem.h>
|
||||
|
||||
#include "backend.h"
|
||||
|
||||
#define WASAPI_BUFFER_COUNT 3
|
||||
#define TARGET_REFTIME (100000) // 10 ms
|
||||
|
||||
struct WaveOutBackend final : AudioBackend {
|
||||
public:
|
||||
~WaveOutBackend() final = default;
|
||||
|
||||
HRESULT init(uint32_t buffer_size);
|
||||
|
||||
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||
|
||||
HRESULT on_initialize(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
DWORD *StreamFlags,
|
||||
REFERENCE_TIME *hnsBufferDuration,
|
||||
REFERENCE_TIME *hnsPeriodicity,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
LPCGUID AudioSessionGuid) noexcept override;
|
||||
|
||||
HRESULT on_get_buffer_size(uint32_t *buffer_frames) noexcept override;
|
||||
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) noexcept override;
|
||||
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept override;
|
||||
|
||||
HRESULT on_is_format_supported(
|
||||
AUDCLNT_SHAREMODE *ShareMode,
|
||||
const WAVEFORMATEX *pFormat,
|
||||
WAVEFORMATEX **ppClosestMatch) noexcept override;
|
||||
|
||||
HRESULT on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept override;
|
||||
|
||||
HRESULT on_get_device_period(
|
||||
REFERENCE_TIME *default_device_period,
|
||||
REFERENCE_TIME *minimum_device_period) override;
|
||||
|
||||
HRESULT on_start() noexcept override;
|
||||
HRESULT on_stop() noexcept override;
|
||||
HRESULT on_set_event_handle(HANDLE *event_handle) override;
|
||||
|
||||
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
|
||||
HRESULT on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) override;
|
||||
|
||||
private:
|
||||
WrappedIAudioClient *client;
|
||||
|
||||
bool initialized = false;
|
||||
HANDLE relay_event = nullptr;
|
||||
HANDLE dispatcher_event = nullptr;
|
||||
HWAVEOUT handle = nullptr;
|
||||
WAVEHDR hdrs[WASAPI_BUFFER_COUNT] {};
|
||||
BYTE *active_sound_buffer = nullptr;
|
||||
};
|
||||
80
hooks/audio/util.cpp
Normal file
80
hooks/audio/util.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "util.h"
|
||||
|
||||
#include <ks.h>
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "util/flags_helper.h"
|
||||
#include "util/logging.h"
|
||||
#include "util/utils.h"
|
||||
|
||||
std::string channel_mask_str(DWORD channel_mask) {
|
||||
FLAGS_START(channel_mask);
|
||||
FLAG(channel_mask, SPEAKER_FRONT_LEFT);
|
||||
FLAG(channel_mask, SPEAKER_FRONT_RIGHT);
|
||||
FLAG(channel_mask, SPEAKER_FRONT_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_LOW_FREQUENCY);
|
||||
FLAG(channel_mask, SPEAKER_BACK_LEFT);
|
||||
FLAG(channel_mask, SPEAKER_BACK_RIGHT);
|
||||
FLAG(channel_mask, SPEAKER_FRONT_LEFT_OF_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_FRONT_RIGHT_OF_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_BACK_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_SIDE_LEFT);
|
||||
FLAG(channel_mask, SPEAKER_SIDE_RIGHT);
|
||||
FLAG(channel_mask, SPEAKER_TOP_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_TOP_FRONT_LEFT);
|
||||
FLAG(channel_mask, SPEAKER_TOP_FRONT_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_TOP_FRONT_RIGHT);
|
||||
FLAG(channel_mask, SPEAKER_TOP_BACK_LEFT);
|
||||
FLAG(channel_mask, SPEAKER_TOP_BACK_CENTER);
|
||||
FLAG(channel_mask, SPEAKER_TOP_BACK_RIGHT);
|
||||
FLAGS_END(channel_mask);
|
||||
}
|
||||
|
||||
std::string share_mode_str(AUDCLNT_SHAREMODE share_mode) {
|
||||
switch (share_mode) {
|
||||
ENUM_VARIANT(AUDCLNT_SHAREMODE_SHARED);
|
||||
ENUM_VARIANT(AUDCLNT_SHAREMODE_EXCLUSIVE);
|
||||
default:
|
||||
return fmt::format("ShareMode(0x{:08x})", share_mode);
|
||||
}
|
||||
}
|
||||
|
||||
void copy_wave_format(WAVEFORMATEXTENSIBLE *destination, const WAVEFORMATEX *source) {
|
||||
if (source->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
memcpy(destination, source, sizeof(WAVEFORMATEXTENSIBLE));
|
||||
} else {
|
||||
memcpy(destination, source, sizeof(WAVEFORMATEX));
|
||||
}
|
||||
}
|
||||
|
||||
void print_format(const WAVEFORMATEX *pFormat) {
|
||||
log_info("audio", "Wave Format:");
|
||||
|
||||
// format specific
|
||||
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
|
||||
log_info("audio", "... SubFormat : {}", guid2s(format->SubFormat));
|
||||
} else {
|
||||
log_info("audio", "... wFormatTag : {}", pFormat->wFormatTag);
|
||||
}
|
||||
|
||||
// generic
|
||||
log_info("audio", "... nChannels : {}", pFormat->nChannels);
|
||||
log_info("audio", "... nSamplesPerSec : {}", pFormat->nSamplesPerSec);
|
||||
log_info("audio", "... nAvgBytesPerSec : {}", pFormat->nAvgBytesPerSec);
|
||||
log_info("audio", "... nBlockAlign : {}", pFormat->nBlockAlign);
|
||||
log_info("audio", "... wBitsPerSample : {}", pFormat->wBitsPerSample);
|
||||
|
||||
// format specific
|
||||
if (pFormat->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
|
||||
auto format = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(pFormat);
|
||||
|
||||
if (pFormat->wBitsPerSample == 0) {
|
||||
log_info("audio", "... wSamplesPerBlock : {}", format->Samples.wSamplesPerBlock);
|
||||
} else {
|
||||
log_info("audio", "... wValidBitsPerSample : {}", format->Samples.wValidBitsPerSample);
|
||||
}
|
||||
|
||||
log_info("audio", "... dwChannelMask : {}", channel_mask_str(format->dwChannelMask));
|
||||
}
|
||||
}
|
||||
12
hooks/audio/util.h
Normal file
12
hooks/audio/util.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
#include <audiosessiontypes.h>
|
||||
#include <mmreg.h>
|
||||
|
||||
std::string channel_mask_str(DWORD channel_mask);
|
||||
std::string share_mode_str(AUDCLNT_SHAREMODE share_mode);
|
||||
void copy_wave_format(WAVEFORMATEXTENSIBLE *destination, const WAVEFORMATEX *source);
|
||||
void print_format(const WAVEFORMATEX *pFormat);
|
||||
Reference in New Issue
Block a user