#define NOMINMAX #include #include #include #include #include #include #include #include #include "capture_target.h" #include "process_audio.h" #include #include #include #include #include #include using Microsoft::WRL::ComPtr; using Microsoft::WRL::FtmBase; using Microsoft::WRL::Make; using Microsoft::WRL::RuntimeClass; using Microsoft::WRL::RuntimeClassFlags; using Microsoft::WRL::ClassicCom; namespace screener::capture { namespace { class ActivationHandler final : public RuntimeClass< RuntimeClassFlags, FtmBase, IActivateAudioInterfaceCompletionHandler> { public: explicit ActivationHandler(HANDLE completed) : completed_(completed) {} STDMETHODIMP ActivateCompleted(IActivateAudioInterfaceAsyncOperation* operation) override { ComPtr activated; HRESULT activationResult = E_UNEXPECTED; result_ = operation->GetActivateResult(&activationResult, &activated); if (SUCCEEDED(result_)) result_ = activationResult; if (SUCCEEDED(result_)) result_ = activated.As(&client_); SetEvent(completed_); return S_OK; } HRESULT Result() const { return result_; } ComPtr Client() const { return client_; } private: HANDLE completed_ = nullptr; HRESULT result_ = E_UNEXPECTED; ComPtr client_; }; HRESULT ActivateProcessLoopback(DWORD pid, HANDLE completed, ComPtr* client) { AUDIOCLIENT_ACTIVATION_PARAMS parameters{}; parameters.ActivationType = AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK; parameters.ProcessLoopbackParams.TargetProcessId = pid; parameters.ProcessLoopbackParams.ProcessLoopbackMode = PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE; PROPVARIANT variant{}; variant.vt = VT_BLOB; variant.blob.cbSize = sizeof(parameters); variant.blob.pBlobData = reinterpret_cast(¶meters); auto handler = Make(completed); if (!handler) return E_OUTOFMEMORY; ComPtr operation; HRESULT result = ActivateAudioInterfaceAsync(VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK, __uuidof(IAudioClient), &variant, handler.Get(), &operation); if (FAILED(result)) return result; if (WaitForSingleObject(completed, 10'000) != WAIT_OBJECT_0) return HRESULT_FROM_WIN32(WAIT_TIMEOUT); result = handler->Result(); if (SUCCEEDED(result)) *client = handler->Client(); return result; } HRESULT ActivateSystemLoopback(ComPtr* client) { ComPtr enumerator; HRESULT result = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator)); ComPtr device; if (SUCCEEDED(result)) { result = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, &device); } if (SUCCEEDED(result)) { result = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, reinterpret_cast(client->GetAddressOf())); } return result; } HRESULT CaptureLoopbackAudio(ComPtr client, HANDLE process, HANDLE stop_event, const StopProbe& stop_probe, const ReadyWriter& ready_writer, const PCMWriter& writer) { if (stop_event == nullptr || !stop_probe || !ready_writer || !writer) { return HRESULT_FROM_WIN32(ERROR_INVALID_PARAMETER); } HANDLE sampleReady = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (sampleReady == nullptr) { const HRESULT result = HRESULT_FROM_WIN32(GetLastError()); return result; } HRESULT result = S_OK; ComPtr capture; WAVEFORMATEX format{}; format.wFormatTag = WAVE_FORMAT_PCM; format.nChannels = kAudioChannels; format.nSamplesPerSec = kAudioSampleRate; format.wBitsPerSample = kAudioBytesPerSample * 8; format.nBlockAlign = kAudioChannels * kAudioBytesPerSample; format.nAvgBytesPerSec = kAudioSampleRate * format.nBlockAlign; if (SUCCEEDED(result)) { result = client->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_LOOPBACK | AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY, 0, 0, &format, nullptr); } if (SUCCEEDED(result)) result = client->GetService(IID_PPV_ARGS(&capture)); if (SUCCEEDED(result)) result = client->SetEventHandle(sampleReady); if (SUCCEEDED(result)) result = client->Start(); if (SUCCEEDED(result)) result = ready_writer(); if (SUCCEEDED(result)) { std::array silence{}; result = writer(0, silence.data(), static_cast(silence.size())); } std::vector pending; size_t consumed = 0; UINT64 nextTimestamp = 0; const HANDLE process_waits[] = {process, sampleReady, stop_event}; const HANDLE system_waits[] = {sampleReady, stop_event}; while (SUCCEEDED(result)) { if (stop_probe()) { result = S_OK; break; } const DWORD wait = process != nullptr ? WaitForMultipleObjects(3, process_waits, FALSE, 1'000) : WaitForMultipleObjects(2, system_waits, FALSE, 1'000); if (process != nullptr && wait == WAIT_OBJECT_0) { result = HRESULT_FROM_WIN32(ERROR_PROCESS_ABORTED); break; } if (wait == WAIT_TIMEOUT) continue; const DWORD sample_index = process != nullptr ? WAIT_OBJECT_0 + 1 : WAIT_OBJECT_0; const DWORD stop_index = process != nullptr ? WAIT_OBJECT_0 + 2 : WAIT_OBJECT_0 + 1; if (wait == stop_index) { result = S_OK; break; } if (wait != sample_index) { result = HRESULT_FROM_WIN32(GetLastError()); break; } UINT32 frames = 0; while (SUCCEEDED(result = capture->GetNextPacketSize(&frames)) && frames > 0) { BYTE* data = nullptr; DWORD flags = 0; UINT64 devicePosition = 0; UINT64 qpcPosition = 0; result = capture->GetBuffer(&data, &frames, &flags, &devicePosition, &qpcPosition); if (FAILED(result)) break; const size_t bytes = static_cast(frames) * format.nBlockAlign; if ((flags & AUDCLNT_BUFFERFLAGS_TIMESTAMP_ERROR) != 0) { capture->ReleaseBuffer(frames); continue; } if ((flags & AUDCLNT_BUFFERFLAGS_DATA_DISCONTINUITY) != 0) { pending.clear(); consumed = 0; } if (pending.size() == consumed) { pending.clear(); consumed = 0; nextTimestamp = qpcPosition; } if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0) { pending.insert(pending.end(), bytes, 0); } else { pending.insert(pending.end(), data, data + bytes); } capture->ReleaseBuffer(frames); while (pending.size() - consumed >= kAudioBytesPerChunk) { result = writer(nextTimestamp, pending.data() + consumed, kAudioBytesPerChunk); if (FAILED(result)) break; consumed += kAudioBytesPerChunk; nextTimestamp += kAudioChunkDuration100ns; } if (FAILED(result)) break; if (consumed > kAudioBytesPerChunk * 4) { pending.erase(pending.begin(), pending.begin() + static_cast(consumed)); consumed = 0; } } } if (client) client->Stop(); CloseHandle(sampleReady); return result; } } // namespace bool ProcessAudioAvailable() { HANDLE completed = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (completed == nullptr) return false; ComPtr client; const HRESULT result = ActivateProcessLoopback(GetCurrentProcessId(), completed, &client); CloseHandle(completed); return SUCCEEDED(result) && client != nullptr; } bool SystemAudioAvailable() { ComPtr client; return SUCCEEDED(ActivateSystemLoopback(&client)) && client != nullptr; } HRESULT CaptureProcessAudio(DWORD pid, UINT64 expectedCreationTime, HANDLE stop_event, const StopProbe& stop_probe, const ReadyWriter& ready_writer, const PCMWriter& writer) { HRESULT com_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(com_result)) return com_result; HRESULT result = ValidateProcessTarget(pid, expectedCreationTime); HANDLE process = nullptr; HANDLE completed = nullptr; ComPtr client; if (SUCCEEDED(result)) { process = OpenProcess(SYNCHRONIZE, FALSE, pid); if (process == nullptr) result = HRESULT_FROM_WIN32(GetLastError()); } if (SUCCEEDED(result)) { completed = CreateEventW(nullptr, FALSE, FALSE, nullptr); if (completed == nullptr) result = HRESULT_FROM_WIN32(GetLastError()); } if (SUCCEEDED(result)) { result = ActivateProcessLoopback(pid, completed, &client); } if (SUCCEEDED(result)) { result = CaptureLoopbackAudio(client, process, stop_event, stop_probe, ready_writer, writer); } if (completed != nullptr) CloseHandle(completed); if (process != nullptr) CloseHandle(process); CoUninitialize(); return result; } HRESULT CaptureSystemAudio(HANDLE stop_event, const StopProbe& stop_probe, const ReadyWriter& ready_writer, const PCMWriter& writer) { HRESULT com_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED); if (FAILED(com_result)) return com_result; ComPtr client; HRESULT result = ActivateSystemLoopback(&client); if (SUCCEEDED(result)) { result = CaptureLoopbackAudio(client, nullptr, stop_event, stop_probe, ready_writer, writer); } CoUninitialize(); return result; } } // namespace screener::capture