1// SoundpadAPO — Audio Processing Object that mixes soundpad audio into microphone capture. 2// Uses multiple inheritance for correct COM vtable layout per interface. 3// Communicates with C# app via Global shared memory. 4 5#define WIN32_LEAN_AND_MEAN 6#include <windows.h> 7#include <unknwn.h> 8 9// ===== Interface GUIDs ===== 10static const GUID IID_IAPO = {0xFD7F2B29,0x24D0,0x4B5C,{0xB1,0x77,0x59,0x2C,0x39,0xF9,0xCA,0x10}}; 11static const GUID IID_IAPRT = {0x9E1D6A6D,0xDDBC,0x4E95,{0xA4,0xC7,0xAD,0x64,0xBA,0x37,0x84,0x6C}}; 12static const GUID IID_IAPCfg = {0x0E5ED805,0xABA6,0x49C3,{0x8F,0x9A,0x2B,0x8C,0x88,0x9C,0x4F,0xA8}}; 13static const GUID IID_IASE = {0x5FA00F27,0xADD6,0x499A,{0x8A,0x9D,0x6B,0x98,0x52,0x1F,0xA7,0x5B}}; 14static const GUID CLSID_SoundpadAPO = {0xA1B2C3D4,0x1234,0x5678,{0x9A,0xBC,0xDE,0xF0,0x12,0x34,0x56,0x78}}; 15 16#define SHM_NAME L"Global\\WC_Soundpad_Buf" 17#define SHM_HEADER_FLOATS 5 18typedef long long HNSTIME; 19 20typedef struct { UINT_PTR pBuffer; UINT32 u32ValidFrameCount; UINT32 u32BufferFlags; UINT32 u32Signature; } APO_CONNECTION_PROPERTY; 21typedef struct { UINT_PTR pFormat; UINT_PTR pBuffer; UINT32 u32MaxFrameCount; } APO_CONNECTION_DESCRIPTOR; 22typedef struct { GUID clsidAPO; DWORD Flags; WCHAR szFriendlyName[256]; WCHAR szCopyrightInfo[256]; 23 DWORD u32MajorVersion, u32MinorVersion, u32MinInputConnections, u32MaxInputConnections, 24 u32MinOutputConnections, u32MaxOutputConnections, u32MaxInstances, u32NumAPOInterfaces; GUID iidAPOInterfaceList[1]; } APO_REG_PROPERTIES; 25 26// ===== Abstract interfaces with correct vtable order ===== 27class __declspec(novtable) IAudioProcessingObject : public IUnknown { 28public: 29 virtual HRESULT STDMETHODCALLTYPE Reset() = 0; 30 virtual HRESULT STDMETHODCALLTYPE GetLatency(HNSTIME*) = 0; 31 virtual HRESULT STDMETHODCALLTYPE GetRegistrationProperties(APO_REG_PROPERTIES**) = 0; 32 virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32, BYTE*) = 0; 33 virtual HRESULT STDMETHODCALLTYPE IsInputFormatSupported(IUnknown*, IUnknown*, IUnknown**) = 0; 34 virtual HRESULT STDMETHODCALLTYPE IsOutputFormatSupported(IUnknown*, IUnknown*, IUnknown**) = 0; 35 virtual HRESULT STDMETHODCALLTYPE GetInputChannelCount(UINT32*) = 0; 36}; 37class __declspec(novtable) IAudioProcessingObjectRT : public IUnknown { 38public: 39 virtual void STDMETHODCALLTYPE APOProcess(UINT32, APO_CONNECTION_PROPERTY**, UINT32, APO_CONNECTION_PROPERTY**) = 0; 40 virtual UINT32 STDMETHODCALLTYPE CalcInputFrames(UINT32) = 0; 41 virtual UINT32 STDMETHODCALLTYPE CalcOutputFrames(UINT32) = 0; 42}; 43class __declspec(novtable) IAudioProcessingObjectConfiguration : public IUnknown { 44public: 45 virtual HRESULT STDMETHODCALLTYPE LockForProcess(UINT32, APO_CONNECTION_DESCRIPTOR**, UINT32, APO_CONNECTION_DESCRIPTOR**) = 0; 46 virtual HRESULT STDMETHODCALLTYPE UnlockForProcess() = 0; 47}; 48class __declspec(novtable) IAudioSystemEffects : public IUnknown {}; 49 50static HMODULE g_hModule = NULL; 51static LONG g_cRefDll = 0; 52static int g_apoLogCount = 0; 53 54static void ApoLog(const char* msg) { 55 if (g_apoLogCount > 200) return; // limit log size 56 g_apoLogCount++; 57 // Build %LOCALAPPDATA%\WindowCapture\apo_native.log. Was hardcoded to C:\Users\Admin\..., 58 // which broke APO logging for every other user account / machine (the APO runs inside 59 // audiodg.exe under the logged-in user). Matches the C# side (SoundpadForm). 60 WCHAR base[MAX_PATH]; 61 DWORD n = GetEnvironmentVariableW(L"LOCALAPPDATA", base, MAX_PATH); 62 if (n == 0 || n + 32 >= MAX_PATH) return; // no/too-long LOCALAPPDATA — skip logging 63 WCHAR dir[MAX_PATH]; wsprintfW(dir, L"%s\\WindowCapture", base); 64 CreateDirectoryW(dir, NULL); // ensure folder exists (harmless if it already does) 65 WCHAR path[MAX_PATH]; wsprintfW(path, L"%s\\apo_native.log", dir); 66 HANDLE hFile = CreateFileW(path, 67 FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); 68 if (hFile != INVALID_HANDLE_VALUE) { 69 SYSTEMTIME st; GetLocalTime(&st); 70 char buf[512]; 71 int len = wsprintfA(buf, "%02d:%02d:%02d.%03d %s\r\n", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, msg); 72 DWORD written; WriteFile(hFile, buf, len, &written, NULL); 73 CloseHandle(hFile); 74 } 75} 76 77// ===== SoundpadAPO — multiple inheritance ===== 78class SoundpadAPO : public IAudioProcessingObject, public IAudioProcessingObjectRT, 79 public IAudioProcessingObjectConfiguration, public IAudioSystemEffects { 80 LONG m_cRef; UINT32 m_channels; HANDLE m_hMapping; float* m_shmPtr; 81public: 82 SoundpadAPO() : m_cRef(1), m_channels(1), m_hMapping(NULL), m_shmPtr(NULL) { InterlockedIncrement(&g_cRefDll); ApoLog("SoundpadAPO created"); } 83 ~SoundpadAPO() { if (m_shmPtr) UnmapViewOfFile(m_shmPtr); if (m_hMapping) CloseHandle(m_hMapping); InterlockedDecrement(&g_cRefDll); } 84 85 // IUnknown — static_cast returns correct vtable pointer for each interface 86 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) { 87 if (!ppv) return E_POINTER; 88 if (riid == IID_IUnknown) *ppv = static_cast<IAudioProcessingObject*>(this); 89 else if (riid == IID_IAPO) *ppv = static_cast<IAudioProcessingObject*>(this); 90 else if (riid == IID_IAPRT) *ppv = static_cast<IAudioProcessingObjectRT*>(this); 91 else if (riid == IID_IAPCfg) *ppv = static_cast<IAudioProcessingObjectConfiguration*>(this); 92 else if (riid == IID_IASE) *ppv = static_cast<IAudioSystemEffects*>(this); 93 else { *ppv = NULL; return E_NOINTERFACE; } 94 AddRef(); return S_OK; 95 } 96 ULONG STDMETHODCALLTYPE AddRef() { return InterlockedIncrement(&m_cRef); } 97 ULONG STDMETHODCALLTYPE Release() { ULONG r = InterlockedDecrement(&m_cRef); if (r == 0) delete this; return r; } 98 99 // IAudioProcessingObject 100 HRESULT STDMETHODCALLTYPE Reset() { return S_OK; } 101 HRESULT STDMETHODCALLTYPE GetLatency(HNSTIME* p) { if (p) *p = 0; return S_OK; } 102 HRESULT STDMETHODCALLTYPE GetRegistrationProperties(APO_REG_PROPERTIES** pp) { 103 if (!pp) return E_POINTER; 104 auto* p = (APO_REG_PROPERTIES*)CoTaskMemAlloc(sizeof(APO_REG_PROPERTIES)); 105 if (!p) return E_OUTOFMEMORY; ZeroMemory(p, sizeof(*p)); 106 p->clsidAPO = CLSID_SoundpadAPO; p->Flags = 0xD; // must match registry Flags 107 wcscpy_s(p->szFriendlyName, L"Soundpad APO"); wcscpy_s(p->szCopyrightInfo, L"WindowCapture"); 108 p->u32MajorVersion = 1; p->u32MinInputConnections = 1; p->u32MaxInputConnections = 1; 109 p->u32MinOutputConnections = 1; p->u32MaxOutputConnections = 1; 110 p->u32MaxInstances = 0xFFFFFFFF; p->u32NumAPOInterfaces = 1; p->iidAPOInterfaceList[0] = IID_IASE; 111 *pp = p; return S_OK; 112 } 113 HRESULT STDMETHODCALLTYPE Initialize(UINT32 cb, BYTE* pby) { ApoLog("Initialize called"); return S_OK; } 114 HRESULT STDMETHODCALLTYPE IsInputFormatSupported(IUnknown*, IUnknown* pReq, IUnknown** ppSupp) { if (ppSupp) { *ppSupp = pReq; if (pReq) pReq->AddRef(); } return S_OK; } 115 HRESULT STDMETHODCALLTYPE IsOutputFormatSupported(IUnknown*, IUnknown* pReq, IUnknown** ppSupp) { if (ppSupp) { *ppSupp = pReq; if (pReq) pReq->AddRef(); } return S_OK; } 116 HRESULT STDMETHODCALLTYPE GetInputChannelCount(UINT32* p) { if (p) *p = m_channels; return S_OK; } 117 118 // IAudioProcessingObjectConfiguration 119 HRESULT STDMETHODCALLTYPE LockForProcess(UINT32, APO_CONNECTION_DESCRIPTOR**, UINT32, APO_CONNECTION_DESCRIPTOR**) { 120 ApoLog("LockForProcess called"); 121 if (!m_hMapping) { 122 m_hMapping = OpenFileMappingW(FILE_MAP_READ|FILE_MAP_WRITE, FALSE, SHM_NAME); 123 if (m_hMapping) { 124 m_shmPtr = (float*)MapViewOfFile(m_hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0); 125 ApoLog(m_shmPtr ? "SHM mapped OK" : "SHM MapViewOfFile FAILED"); 126 } else { 127 char buf[128]; wsprintfA(buf, "SHM OpenFileMapping FAILED err=%d", GetLastError()); ApoLog(buf); 128 } 129 } 130 return S_OK; 131 } 132 HRESULT STDMETHODCALLTYPE UnlockForProcess() { return S_OK; } 133 134 // IAudioProcessingObjectRT 135 int m_processCount; 136 void STDMETHODCALLTYPE APOProcess(UINT32 numIn, APO_CONNECTION_PROPERTY** ppIn, UINT32 numOut, APO_CONNECTION_PROPERTY** ppOut) { 137 m_processCount++; 138 if (m_processCount <= 3) { 139 char buf[256]; wsprintfA(buf, "APOProcess call #%d numIn=%d numOut=%d shmPtr=%p vol=%.2f", 140 m_processCount, numIn, numOut, m_shmPtr, m_shmPtr ? m_shmPtr[0] : -1.0f); 141 ApoLog(buf); 142 } 143 if (numIn < 1 || numOut < 1 || !ppIn || !ppOut || !ppIn[0] || !ppOut[0]) return; 144 float* pIn = (float*)(ppIn[0]->pBuffer); float* pOut = (float*)(ppOut[0]->pBuffer); 145 UINT32 frames = ppIn[0]->u32ValidFrameCount; UINT32 total = frames * m_channels; 146 if (ppIn[0]->u32BufferFlags == 1) ZeroMemory(pOut, total * sizeof(float)); 147 else if (pIn != pOut) CopyMemory(pOut, pIn, total * sizeof(float)); 148 ppOut[0]->u32ValidFrameCount = frames; ppOut[0]->u32BufferFlags = ppIn[0]->u32BufferFlags; 149 150 if (!m_shmPtr && !m_hMapping) { m_hMapping = OpenFileMappingW(FILE_MAP_READ|FILE_MAP_WRITE, FALSE, SHM_NAME); 151 if (m_hMapping) m_shmPtr = (float*)MapViewOfFile(m_hMapping, FILE_MAP_READ|FILE_MAP_WRITE, 0, 0, 0); } 152 if (m_shmPtr && m_shmPtr[0] > 0.0001f) { 153 float vol = m_shmPtr[0]; int rp = (int)m_shmPtr[1]; int ts = (int)m_shmPtr[2]; 154 float* smp = m_shmPtr + SHM_HEADER_FLOATS; 155 UINT32 mix = total; if (rp + (int)mix > ts) mix = (ts > rp) ? (UINT32)(ts - rp) : 0; 156 if (mix > 0) { for (UINT32 i = 0; i < mix; i++) pOut[i] += smp[rp + i] * vol; 157 m_shmPtr[1] = (float)(rp + mix); ppOut[0]->u32BufferFlags = 0; } 158 if (rp + (int)mix >= ts) m_shmPtr[0] = 0; 159 } 160 } 161 UINT32 STDMETHODCALLTYPE CalcInputFrames(UINT32 f) { return f; } 162 UINT32 STDMETHODCALLTYPE CalcOutputFrames(UINT32 f) { return f; } 163}; 164 165// ===== Class Factory ===== 166class SoundpadAPOFactory : public IClassFactory { 167 LONG m_cRef; 168public: 169 SoundpadAPOFactory() : m_cRef(1) { InterlockedIncrement(&g_cRefDll); } 170 ~SoundpadAPOFactory() { InterlockedDecrement(&g_cRefDll); } 171 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppv) { if (riid == IID_IUnknown || riid == IID_IClassFactory) { *ppv = this; AddRef(); return S_OK; } *ppv = NULL; return E_NOINTERFACE; } 172 ULONG STDMETHODCALLTYPE AddRef() { return InterlockedIncrement(&m_cRef); } 173 ULONG STDMETHODCALLTYPE Release() { ULONG r = InterlockedDecrement(&m_cRef); if (r == 0) delete this; return r; } 174 HRESULT STDMETHODCALLTYPE CreateInstance(IUnknown* pOuter, REFIID riid, void** ppv) { 175 if (pOuter) return CLASS_E_NOAGGREGATION; 176 auto* apo = new SoundpadAPO(); HRESULT hr = apo->QueryInterface(riid, ppv); apo->Release(); return hr; } 177 HRESULT STDMETHODCALLTYPE LockServer(BOOL f) { if (f) InterlockedIncrement(&g_cRefDll); else InterlockedDecrement(&g_cRefDll); return S_OK; } 178}; 179 180extern "C" { 181BOOL WINAPI DllMain(HINSTANCE h, DWORD r, LPVOID) { if (r == DLL_PROCESS_ATTACH) { g_hModule = h; DisableThreadLibraryCalls(h); } return TRUE; } 182HRESULT STDAPICALLTYPE DllCanUnloadNow() { return (g_cRefDll == 0) ? S_OK : S_FALSE; } 183HRESULT STDAPICALLTYPE DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv) { 184 if (rclsid == CLSID_SoundpadAPO) { auto* f = new SoundpadAPOFactory(); HRESULT hr = f->QueryInterface(riid, ppv); f->Release(); return hr; } 185 return CLASS_E_CLASSNOTAVAILABLE; } 186HRESULT STDAPICALLTYPE DllRegisterServer() { 187 WCHAR path[MAX_PATH]; GetModuleFileNameW(g_hModule, path, MAX_PATH); HKEY hKey; 188 if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\CLSID\\{A1B2C3D4-1234-5678-9ABC-DEF012345678}\\InprocServer32", 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL) == ERROR_SUCCESS) { 189 RegSetValueExW(hKey, NULL, 0, REG_SZ, (BYTE*)path, (DWORD)((wcslen(path)+1)*sizeof(WCHAR))); 190 WCHAR t[] = L"Both"; RegSetValueExW(hKey, L"ThreadingModel", 0, REG_SZ, (BYTE*)t, sizeof(t)); RegCloseKey(hKey); } 191 return S_OK; } 192HRESULT STDAPICALLTYPE DllUnregisterServer() { RegDeleteTreeW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\CLSID\\{A1B2C3D4-1234-5678-9ABC-DEF012345678}"); return S_OK; } 193}