From 8a8da76d4c5929c78b5ab4bfed230af2d04111f7 Mon Sep 17 00:00:00 2001 From: Andrey Kryuchenko Date: Sat, 18 Apr 2026 21:01:09 +0200 Subject: [PATCH] refactor: extract WASAPI capture into AudioCapture class Step 3 of the cli_args_debugger.cpp decomposition (steps 1 and 2 were #3 and #4). Lifts the entire microphone pipeline out of ArgumentDebuggerWindow into its own class in audio_capture.{hpp,cpp}. What moves: - 11 member fields (device_enumerator_, capture_device_, audio_client_, capture_client_, mix_format_, audio_event_, audio_thread_, mic_level_, mic_available_, audio_thread_running_, mic_name_) - 4 methods (AudioCaptureThread static entry, AudioCaptureThreadImpl, InitializeMicrophone, PollMicrophone) - The two audio-specific hunks of OnDestroy and Cleanup What changes for consumers: - ArgumentDebuggerWindow now holds `AudioCapture audio_capture_` as a regular data member. Initialize() brings the pipeline up; Stop() takes it down cooperatively; IsAvailable() / Level() / Name() feed the meter in RenderFrame. - seh_wrapper.cpp now casts its LPVOID parameter to AudioCapture* and calls ThreadMain() on it. The ArgumentDebuggerWindow forward declaration in that file is gone. PollMicrophone (CCN 50 in the old file) is split: ResolveFormat, PeakFloat32 / PeakPcm16 / PeakPcm24 / PeakPcm32, PeakForFormat, plus a thin PollOnce that sequences GetNextPacketSize / GetBuffer / peak / ReleaseBuffer. PollOnce ends up at CCN 16; the peak helpers are all single-digit. cli_args_debugger.cpp: 1846 -> 1382 lines (-464). Only RenderFrame (CCN 21) remains over the static-analysis threshold in the main file. --- .github/workflows/build.yml | 2 +- CMakeLists.txt | 1 + README.md | 2 +- audio_capture.cpp | 434 +++++++++++++++++++++++++++++++ audio_capture.hpp | 74 ++++++ cli_args_debugger.cpp | 494 ++---------------------------------- seh_wrapper.cpp | 41 ++- tests/CMakeLists.txt | 1 + validate.sh | 2 +- 9 files changed, 548 insertions(+), 503 deletions(-) create mode 100644 audio_capture.cpp create mode 100644 audio_capture.hpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92e6d23..f8cb824 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: shell: cmd run: | cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE /GS /sdl ^ - cli_args_debugger.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp audio_capture.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build\cloud-streaming-args-debugger.exe ^ /Fo:obj\ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib winmm.lib psapi.lib diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d77165..e7d249d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_CURRENT_SOURCE_DIR}/build) add_executable(cloud-streaming-args-debugger WIN32 # Specify that the application uses WinMain instead of main cli_args_debugger.cpp + audio_capture.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp diff --git a/README.md b/README.md index d6efcce..2a7ad74 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Check out a preview of the application: # Compile with MSVC cl /EHsc /std:c++20 /permissive- /I. /DUNICODE /D_UNICODE ^ - cli_args_debugger.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ + cli_args_debugger.cpp audio_capture.cpp log_manager.cpp path_info.cpp seh_wrapper.cpp qrcodegen.cpp ^ /Fe:build/ArgumentDebugger.exe ^ /Fo:build/ ^ /link d3d11.lib d3dcompiler.lib d2d1.lib dwrite.lib ole32.lib avrt.lib user32.lib shell32.lib gdi32.lib propsys.lib diff --git a/audio_capture.cpp b/audio_capture.cpp new file mode 100644 index 0000000..d9fc94d --- /dev/null +++ b/audio_capture.cpp @@ -0,0 +1,434 @@ +#ifndef UNICODE +#define UNICODE +#define _UNICODE +#endif + +#include "audio_capture.hpp" + +// clang-format off +#include +#include +#include +#include +#include +#include +#include +// clang-format on + +#include +#include + +#include "log_manager.hpp" + +#pragma comment(lib, "ole32") +#pragma comment(lib, "avrt") +#pragma comment(lib, "propsys") + +using Microsoft::WRL::ComPtr; + +// Forward declaration of the SEH-guarded thread entry that hands control +// back to AudioCapture::ThreadMain via static_cast on the parameter. +extern "C" DWORD WINAPI RawAudioThreadWithSEH(LPVOID param) noexcept; + +namespace +{ + +// Helper for DX_CALL-style throw on HRESULT failure. Duplicates the macro +// from cli_args_debugger.cpp to keep this TU independent. +#define AC_CALL(expr, msg) \ + do \ + { \ + HRESULT _hr = (expr); \ + if (FAILED(_hr)) \ + throw std::runtime_error(msg); \ + } while (0) + +struct SampleFormat +{ + WORD tag; + WORD bps; + UINT32 channels; +}; + +// Resolve tag / bits-per-sample for WAVEFORMATEXTENSIBLE, falling back to +// the base WAVEFORMATEX fields for plain PCM/IEEE_FLOAT streams. +SampleFormat ResolveFormat(const WAVEFORMATEX* mix) +{ + SampleFormat sf{mix->wFormatTag, mix->wBitsPerSample, mix->nChannels}; + + if (sf.tag == WAVE_FORMAT_EXTENSIBLE) + { + if (mix->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) + { + auto* wfex = reinterpret_cast(mix); + if (IsEqualGUID(wfex->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) + sf.tag = WAVE_FORMAT_IEEE_FLOAT; + else if (IsEqualGUID(wfex->SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) + sf.tag = WAVE_FORMAT_PCM; + else + Log(L"Unknown SubFormat GUID in WAVE_FORMAT_EXTENSIBLE"); + + sf.bps = + wfex->Samples.wValidBitsPerSample ? wfex->Samples.wValidBitsPerSample : wfex->Format.wBitsPerSample; + } + else + { + Log(L"WAVE_FORMAT_EXTENSIBLE with invalid cbSize: " + std::to_wstring(mix->cbSize)); + sf.tag = WAVE_FORMAT_PCM; + sf.bps = mix->wBitsPerSample; + } + } + if (sf.channels == 0) + { + Log(L"Invalid channel count: 0"); + sf.channels = 1; + } + return sf; +} + +float PeakFloat32(const BYTE* data, UINT32 total_samples) +{ + float peak = 0.f; + const auto* samples = reinterpret_cast(data); + for (UINT32 i = 0; i < total_samples; ++i) + { + float v = samples[i]; + if (v < 0) + v = -v; + if (v > peak) + peak = v; + } + return peak; +} + +float PeakPcm16(const BYTE* data, UINT32 total_samples) +{ + float peak = 0.f; + const auto* samples = reinterpret_cast(data); + for (UINT32 i = 0; i < total_samples; ++i) + { + float v = samples[i] / 32768.0f; + if (v < 0) + v = -v; + if (v > peak) + peak = v; + } + return peak; +} + +float PeakPcm24(const BYTE* data, UINT32 total_samples) +{ + float peak = 0.f; + const auto* p = reinterpret_cast(data); + for (UINT32 i = 0; i < total_samples; ++i) + { + int32_t sample = (p[3 * i] << 8) | (p[3 * i + 1] << 16) | (p[3 * i + 2] << 24); + sample >>= 8; + float v = sample / 8388608.0f; + if (v < 0) + v = -v; + if (v > peak) + peak = v; + } + return peak; +} + +float PeakPcm32(const BYTE* data, UINT32 total_samples) +{ + float peak = 0.f; + const auto* samples = reinterpret_cast(data); + for (UINT32 i = 0; i < total_samples; ++i) + { + float v = samples[i] / 2147483648.0f; + if (v < 0) + v = -v; + if (v > peak) + peak = v; + } + return peak; +} + +float PeakForFormat(const SampleFormat& sf, const BYTE* data, UINT32 total_samples) +{ + if (sf.tag == WAVE_FORMAT_IEEE_FLOAT && sf.bps == 32) + return PeakFloat32(data, total_samples); + if (sf.tag == WAVE_FORMAT_PCM && sf.bps == 16) + return PeakPcm16(data, total_samples); + if (sf.tag == WAVE_FORMAT_PCM && sf.bps == 24) + return PeakPcm24(data, total_samples); + if (sf.tag == WAVE_FORMAT_PCM && sf.bps == 32) + return PeakPcm32(data, total_samples); + + Log(L"Unsupported audio format: tag=" + std::to_wstring(sf.tag) + L", bps=" + std::to_wstring(sf.bps)); + return 0.f; +} + +} // namespace + +AudioCapture::~AudioCapture() +{ + Stop(); + + capture_client_.Reset(); + audio_client_.Reset(); + capture_device_.Reset(); + device_enumerator_.Reset(); + + if (mix_format_) + { + CoTaskMemFree(mix_format_); + mix_format_ = nullptr; + } +} + +bool AudioCapture::Initialize() +{ + try + { + AC_CALL(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&device_enumerator_)), + "IMMDeviceEnumerator failed"); + AC_CALL(device_enumerator_->GetDefaultAudioEndpoint(eCapture, eConsole, capture_device_.GetAddressOf()), + "No default capture device"); + + ComPtr store; + AC_CALL(capture_device_->OpenPropertyStore(STGM_READ, &store), "OpenPropertyStore failed"); + PROPVARIANT pv; + PropVariantInit(&pv); + AC_CALL(store->GetValue(PKEY_Device_FriendlyName, &pv), "GetValue(FriendlyName) failed"); + mic_name_ = pv.vt == VT_LPWSTR ? pv.pwszVal : L"Unknown microphone"; + PropVariantClear(&pv); + + mic_available_ = true; + + AC_CALL(capture_device_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, + reinterpret_cast(audio_client_.GetAddressOf())), + "IAudioClient activate failed"); + + AC_CALL(audio_client_->GetMixFormat(&mix_format_), "GetMixFormat failed"); + + // 100ms buffer for smoother level visualisation. + REFERENCE_TIME buf_dur = 100 * 10000; + AC_CALL(audio_client_->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buf_dur, 0, + mix_format_, nullptr), + "AudioClient init failed"); + AC_CALL(audio_client_->GetService(IID_PPV_ARGS(&capture_client_)), "GetService(IAudioCaptureClient)"); + + audio_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!audio_event_) + throw std::runtime_error("Failed to create audio event"); + audio_client_->SetEventHandle(audio_event_); + + thread_running_.store(true); + audio_thread_ = CreateThread(nullptr, 0, RawAudioThreadWithSEH, this, 0, nullptr); + if (!audio_thread_) + { + thread_running_.store(false); + throw std::runtime_error("CreateThread failed for audio capture"); + } + + Log(L"Microphone initialized successfully"); + return true; + } + catch (const std::exception& ex) + { + Log(L"Microphone initialization failed: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); + mic_available_ = false; + return false; + } +} + +void AudioCapture::Stop(DWORD timeout_ms) +{ + thread_running_.store(false); + if (audio_event_) + SetEvent(audio_event_); + + if (audio_thread_) + { + DWORD wait_result = WaitForSingleObject(audio_thread_, timeout_ms); + if (wait_result == WAIT_TIMEOUT) + { + Log(L"WARNING: Audio thread did not terminate gracefully, forcing termination"); + TerminateThread(audio_thread_, 0); + } + CloseHandle(audio_thread_); + audio_thread_ = nullptr; + } + + if (audio_event_) + { + CloseHandle(audio_event_); + audio_event_ = nullptr; + } + + if (audio_client_) + { + try + { + audio_client_->Stop(); + Log(L"Audio client stopped"); + } + catch (...) + { + Log(L"Exception stopping audio client"); + } + } +} + +DWORD AudioCapture::ThreadMain() +{ + // Each thread needs its own COM apartment. + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) + return 0; + + HANDLE mm_handle = nullptr; + try + { + DWORD task_index = 0; + mm_handle = AvSetMmThreadCharacteristicsW(L"Pro Audio", &task_index); + if (!mm_handle) + Log(L"Warning: AvSetMmThreadCharacteristicsW failed"); + + audio_client_->Start(); + Log(L"Audio capture thread started"); + + while (thread_running_.load()) + { + DWORD wait = WaitForSingleObject(audio_event_, 200); + if (wait == WAIT_OBJECT_0) + { + Log(L"Audio thread: signal received"); + } + else if (wait == WAIT_TIMEOUT) + { + static ULONGLONG last_timeout_log = 0; + ULONGLONG now = GetTickCount64(); + if (now - last_timeout_log > 30000) + { + Log(L"Audio thread: timeout (normal)"); + last_timeout_log = now; + } + } + else + { + Log(L"Audio thread: wait failed, code=" + std::to_wstring(wait)); + } + + PollOnce(); + } + + audio_client_->Stop(); + Log(L"Audio capture thread stopped"); + } + catch (const std::exception& ex) + { + Log(L"Audio thread exception: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); + } + catch (...) + { + Log(L"Audio thread: unknown exception"); + } + + if (mm_handle) + AvRevertMmThreadCharacteristics(mm_handle); + CoUninitialize(); + return 0; +} + +void AudioCapture::PollOnce() +{ + try + { + if (!thread_running_.load() || !capture_client_) + return; + + UINT32 pkt_len = 0; + HRESULT hr = capture_client_->GetNextPacketSize(&pkt_len); + if (FAILED(hr)) + { + Log(L"PollMicrophone: Initial GetNextPacketSize failed, hr=0x" + std::to_wstring(hr)); + return; + } + if (pkt_len == 0) + return; + + while (pkt_len > 0) + { + BYTE* data = nullptr; + UINT32 frames = 0; + DWORD flags = 0; + hr = capture_client_->GetBuffer(&data, &frames, &flags, nullptr, nullptr); + if (FAILED(hr)) + { + Log(L"PollMicrophone: GetBuffer failed, hr=0x" + std::to_wstring(hr)); + break; + } + + if (!data || frames == 0) + { + Log(L"PollMicrophone: Invalid buffer — data=" + std::wstring(data ? L"valid" : L"NULL") + L", frames=" + + std::to_wstring(frames)); + capture_client_->ReleaseBuffer(frames); + break; + } + + float peak = ComputePeak(data, frames, flags); + float current_level = mic_level_.load(); + mic_level_.store(current_level * 0.5f + peak * 0.5f); + + if (!thread_running_.load()) + { + Log(L"PollMicrophone: Thread signaled to exit, breaking"); + capture_client_->ReleaseBuffer(frames); + break; + } + + hr = capture_client_->ReleaseBuffer(frames); + if (FAILED(hr)) + { + Log(L"PollMicrophone: ReleaseBuffer failed, hr=0x" + std::to_wstring(hr)); + break; + } + + if (!thread_running_.load()) + { + Log(L"PollMicrophone: Thread signaled to exit before next packet"); + break; + } + + hr = capture_client_->GetNextPacketSize(&pkt_len); + if (FAILED(hr)) + { + Log(L"PollMicrophone: GetNextPacketSize failed after processing, hr=0x" + std::to_wstring(hr)); + break; + } + } + } + catch (const std::exception& ex) + { + Log(L"PollMicrophone exception: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); + } + catch (...) + { + Log(L"PollMicrophone: unknown exception"); + } +} + +float AudioCapture::ComputePeak(const BYTE* data, UINT32 frames, DWORD flags) const +{ + if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) || !data || !mix_format_) + return 0.f; + + const SampleFormat sf = ResolveFormat(mix_format_); + + UINT64 total64 = static_cast(frames) * static_cast(sf.channels); + if (total64 > UINT32_MAX) + { + Log(L"Sample count overflow: " + std::to_wstring(total64)); + return 0.f; + } + const UINT32 total_samples = static_cast(total64); + + return PeakForFormat(sf, data, total_samples); +} diff --git a/audio_capture.hpp b/audio_capture.hpp new file mode 100644 index 0000000..2f83265 --- /dev/null +++ b/audio_capture.hpp @@ -0,0 +1,74 @@ +#pragma once + +// clang-format off +#include +#include +#include +// clang-format on + +#include +#include + +#include + +// WASAPI microphone capture in shared mode with event-driven pumping on its +// own background thread. The thread body is reached through seh_wrapper.cpp +// so that SEH faults in the audio stack (e.g. device disconnect during +// GetBuffer) can be turned into a logged termination instead of crashing +// the process. +// +// Ownership: the object owns the thread, the event handle and all COM +// interfaces. Destruction calls Stop() — callers usually prefer to call +// Stop() explicitly on shutdown so they can observe any failure. +class AudioCapture +{ + public: + AudioCapture() = default; + ~AudioCapture(); + + AudioCapture(const AudioCapture&) = delete; + AudioCapture& operator=(const AudioCapture&) = delete; + + // Bring up the WASAPI pipeline and spin up the capture thread. Logs and + // returns false on failure instead of throwing — a missing microphone + // must not take down the rest of the UI. + bool Initialize(); + + // Cooperatively stop the capture thread. Signals the event, waits for + // the thread with the given timeout, and (as a last resort) terminates + // it. Safe to call multiple times and from the destructor. + void Stop(DWORD timeout_ms = 5000); + + bool IsAvailable() const + { + return mic_available_.load(); + } + float Level() const + { + return mic_level_.load(); + } + const std::wstring& Name() const + { + return mic_name_; + } + + // Entry point invoked by the SEH wrapper in seh_wrapper.cpp. + // Returns the thread exit code. + DWORD ThreadMain(); + + private: + void PollOnce(); + float ComputePeak(const BYTE* data, UINT32 frames, DWORD flags) const; + + Microsoft::WRL::ComPtr device_enumerator_; + Microsoft::WRL::ComPtr capture_device_; + Microsoft::WRL::ComPtr audio_client_; + Microsoft::WRL::ComPtr capture_client_; + WAVEFORMATEX* mix_format_ = nullptr; + HANDLE audio_event_ = nullptr; + HANDLE audio_thread_ = nullptr; + std::atomic mic_level_{0.f}; + std::atomic mic_available_{false}; + std::atomic thread_running_{false}; + std::wstring mic_name_; +}; diff --git a/cli_args_debugger.cpp b/cli_args_debugger.cpp index 44ebd9f..78ae8fe 100644 --- a/cli_args_debugger.cpp +++ b/cli_args_debugger.cpp @@ -96,6 +96,10 @@ using qrcodegen::QrCode; // Path/env inspection (executable path, OS version, Wine/Proton, etc.) #include "path_info.hpp" +// WASAPI microphone capture (owns its own thread, COM objects, and level +// smoothing). See audio_capture.hpp for the public contract. +#include "audio_capture.hpp" + // Use Microsoft::WRL::ComPtr for COM object management using Microsoft::WRL::ComPtr; @@ -155,8 +159,6 @@ LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lpara ArgumentDebuggerWindow* g_app_instance = nullptr; // External SEH wrapper function (defined in seh_wrapper.cpp) -extern "C" DWORD WINAPI RawAudioThreadWithSEH(LPVOID param) noexcept; - // Global function for unhandled exception filter - renamed to avoid conflict LONG WINAPI AppUnhandledExceptionFilter(EXCEPTION_POINTERS* ep) { @@ -183,9 +185,6 @@ class ArgumentDebuggerWindow return is_running_; } - // Thread implementation with C++ exception handling - public for thread access - DWORD AudioCaptureThreadImpl(LPVOID param); - private: void InitializeWindow(HINSTANCE h_instance, int cmd_show); void InitializeDevice(); @@ -193,15 +192,10 @@ class ArgumentDebuggerWindow void CreateRenderTargetView(); void CreateD2DResources(); void CreateShadersAndGeometry(); - void InitializeMicrophone(); - void PollMicrophone(); void RenderFrame(); void Cleanup(); void UpdateRotation(float delta_time); - // Audio capture thread function with correct calling convention and SEH wrapper - static __declspec(nothrow) DWORD WINAPI AudioCaptureThread(LPVOID param); - private: // Update QR code – here we add the FPS synchronization logic. void UpdateQrCode(ULONGLONG current_time); @@ -276,17 +270,7 @@ class ArgumentDebuggerWindow ComPtr pixel_shader_; // WASAPI - ComPtr device_enumerator_; - ComPtr capture_device_; - ComPtr audio_client_; - ComPtr capture_client_; - WAVEFORMATEX* mix_format_ = nullptr; - HANDLE audio_event_ = nullptr; - HANDLE audio_thread_ = nullptr; - std::atomic mic_level_{0.f}; // 0..1 - std::atomic mic_available_{false}; - std::atomic audio_thread_running_{false}; // Flag for safe thread termination - std::wstring mic_name_; // FriendlyName ("USB Mic (Realtek ...)") + AudioCapture audio_capture_; // Owns the WASAPI pipeline and capture thread }; #ifndef EXCLUDE_MAIN @@ -532,33 +516,9 @@ void ArgumentDebuggerWindow::OnDestroy() { Log(L"Window destroy event"); - // 1. Signal threads to exit via atomic flags is_running_ = false; - audio_thread_running_.store(false); - - // Wake up audio thread if waiting - if (audio_event_) - SetEvent(audio_event_); - - // 2. Wait for thread completion with timeout to prevent hanging - if (audio_thread_) - { - DWORD wait_result = WaitForSingleObject(audio_thread_, 5000); // 5 second timeout - if (wait_result == WAIT_TIMEOUT) - { - Log(L"WARNING: Audio thread did not terminate gracefully, forcing termination"); - TerminateThread(audio_thread_, 0); - } - CloseHandle(audio_thread_); - audio_thread_ = nullptr; - } - if (audio_event_) - { // Close event handle - CloseHandle(audio_event_); - audio_event_ = nullptr; - } - - Cleanup(); // 3. Now safely release COM objects + audio_capture_.Stop(); + Cleanup(); PostQuitMessage(0); } @@ -600,7 +560,7 @@ void ArgumentDebuggerWindow::InitializeDevice() CreateRenderTargetView(); CreateD2DResources(); CreateShadersAndGeometry(); - InitializeMicrophone(); + audio_capture_.Initialize(); } void ArgumentDebuggerWindow::CreateDeviceAndSwapChain(UINT width, UINT height) @@ -1008,9 +968,9 @@ void ArgumentDebuggerWindow::RenderFrame() } // --- Volume Meter (L/R) --- - if (mic_available_) + if (audio_capture_.IsAvailable()) { - float level = mic_level_.load(); // 0..1 + float level = audio_capture_.Level(); // 0..1 // Draw two volume bars - one for left, one for right float bar_w = 30.0f, bar_h = 150.0f; @@ -1020,7 +980,8 @@ void ArgumentDebuggerWindow::RenderFrame() float y0 = size.height - kMargin - bar_h; // Device name title with improved layout - std::wstring dev_title = L"Mic: " + (mic_name_.empty() ? L"" : mic_name_); + const std::wstring& dev_name = audio_capture_.Name(); + std::wstring dev_title = L"Mic: " + (dev_name.empty() ? L"" : dev_name); // Position device name at the right edge of the screen // parameters for the text area @@ -1141,432 +1102,14 @@ void ArgumentDebuggerWindow::UpdateRotation(float delta_time) rotation_angle_ += delta_time * DirectX::XM_PIDIV4 / 2.0f; } -// SEH wrapper function is implemented in seh_wrapper.cpp - -// Entry point that forwards to the raw SEH function -__declspec(nothrow) DWORD WINAPI ArgumentDebuggerWindow::AudioCaptureThread(LPVOID param) -{ - // This is just a thin wrapper to maintain the class method interface - return RawAudioThreadWithSEH(param); -} - -// The actual implementation with C++ exception handling -DWORD ArgumentDebuggerWindow::AudioCaptureThreadImpl(LPVOID param) -{ - // Each thread needs its own COM initialization - HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(hr) && hr != RPC_E_CHANGED_MODE) - return 0; - - auto self = static_cast(param); - HANDLE mmHandle = nullptr; // Multimedia handle for thread priority - - try - { - // Set audio thread priorities - DWORD taskIndex = 0; - mmHandle = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex); - if (!mmHandle) - { - Log(L"Warning: AvSetMmThreadCharacteristicsW failed"); - } - self->audio_client_->Start(); - - Log(L"Audio capture thread started"); - - while (self->audio_thread_running_.load()) - { - DWORD waitResult = WaitForSingleObject(self->audio_event_, 200); - if (waitResult == WAIT_OBJECT_0) - { - Log(L"Audio thread: signal received"); - } - else if (waitResult == WAIT_TIMEOUT) - { - // Log timeouts only once per 30 seconds to avoid log spam - static ULONGLONG lastTimeoutLog = 0; - ULONGLONG now = GetTickCount64(); - if (now - lastTimeoutLog > 30000) - { - Log(L"Audio thread: timeout (normal)"); - lastTimeoutLog = now; - } - } - else - { - Log(L"Audio thread: wait failed, code=" + std::to_wstring(waitResult)); - } - - // Call PollMicrophone directly - SEH handling moved to AudioCaptureThread - self->PollMicrophone(); - } - self->audio_client_->Stop(); - - Log(L"Audio capture thread stopped"); - } - catch (const std::exception& ex) - { - // Log exception from audio thread - Log(L"Audio thread exception: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); - } - catch (...) - { - // Log unknown exception from audio thread - Log(L"Audio thread: unknown exception"); - } - - // Revert thread characteristics if we set them - if (mmHandle) - { - AvRevertMmThreadCharacteristics(mmHandle); - } - - CoUninitialize(); - return 0; -} - -void ArgumentDebuggerWindow::InitializeMicrophone() -{ - // No COM initialization here - it's already done in wWinMain - - try - { - DX_CALL(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&device_enumerator_)), - "IMMDeviceEnumerator failed"); - - DX_CALL(device_enumerator_->GetDefaultAudioEndpoint(eCapture, eConsole, capture_device_.GetAddressOf()), - "No default capture device"); - - // --- FriendlyName ----------------------------------------------------------------- - ComPtr store; - DX_CALL(capture_device_->OpenPropertyStore(STGM_READ, &store), "OpenPropertyStore failed"); - - PROPVARIANT pv; - PropVariantInit(&pv); - DX_CALL(store->GetValue(PKEY_Device_FriendlyName, &pv), "GetValue(FriendlyName) failed"); - - mic_name_ = pv.vt == VT_LPWSTR ? pv.pwszVal : L"Unknown microphone"; - PropVariantClear(&pv); - // ---------------------------------------------------------------------------------- - - mic_available_ = true; - - DX_CALL(capture_device_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, - reinterpret_cast(audio_client_.GetAddressOf())), - "IAudioClient activate failed"); - - DX_CALL(audio_client_->GetMixFormat(&mix_format_), "GetMixFormat failed"); - - // Increased buffer size to 100ms for smoother visualization - REFERENCE_TIME buf_dur = 100 * 10000; // 1ms = 10,000 * 100ns - DX_CALL(audio_client_->Initialize(AUDCLNT_SHAREMODE_SHARED, AUDCLNT_STREAMFLAGS_EVENTCALLBACK, buf_dur, 0, - mix_format_, nullptr), - "AudioClient init failed"); - - DX_CALL(audio_client_->GetService(IID_PPV_ARGS(&capture_client_)), "GetService(IAudioCaptureClient)"); - - audio_event_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); - if (audio_event_ == nullptr) - { - throw std::runtime_error("Failed to create audio event"); - } - audio_client_->SetEventHandle(audio_event_); - - // Set running flag before starting the thread - audio_thread_running_.store(true); - - // Audio capture thread: use SEH-wrapped version - audio_thread_ = CreateThread(nullptr, 0, ArgumentDebuggerWindow::AudioCaptureThread, this, 0, nullptr); - - Log(L"Microphone initialized successfully"); - } - catch (const std::exception& ex) - { - Log(L"Microphone initialization failed: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); - mic_available_ = false; // Will display "No microphone detected" - return; // Don't create thread if initialization failed - } -} - -void ArgumentDebuggerWindow::PollMicrophone() -{ - try - { - // Quick check to bail out if thread should terminate - if (!audio_thread_running_.load()) - { - return; - } - - // Safety check for capture_client_ pointer - if (!capture_client_) - { - Log(L"PollMicrophone: capture_client_ is NULL"); - return; - } - - UINT32 pkt_len = 0; - HRESULT hr = capture_client_->GetNextPacketSize(&pkt_len); - if (FAILED(hr)) - { - Log(L"PollMicrophone: Initial GetNextPacketSize failed, hr=0x" + std::to_wstring(hr)); - return; - } - - // No packets available - if (pkt_len == 0) - { - return; - } - - while (pkt_len > 0) - { - BYTE* data = nullptr; - UINT32 frames = 0; - DWORD flags = 0; - hr = capture_client_->GetBuffer(&data, &frames, &flags, nullptr, nullptr); - - if (FAILED(hr)) - { - Log(L"PollMicrophone: GetBuffer failed, hr=0x" + std::to_wstring(hr)); - break; - } - - // Bail out if buffer is invalid - if (data == nullptr || frames == 0) - { - std::wstring dataStatus = data ? L"valid" : L"NULL"; - Log(L"PollMicrophone: Invalid buffer - data=" + dataStatus + L", frames=" + std::to_wstring(frames)); - - // Still need to release the buffer even if data is NULL - if (SUCCEEDED(hr)) - { - capture_client_->ReleaseBuffer(frames); - } - break; - } - - // Log buffer details - if (flags & AUDCLNT_BUFFERFLAGS_SILENT) - { - Log(L"PollMicrophone: Silent buffer detected, frames=" + std::to_wstring(frames) + L", flags=0x" + - std::to_wstring(flags)); - } - else - { - Log(L"PollMicrophone: Buffer received: frames=" + std::to_wstring(frames) + L", flags=0x" + - std::to_wstring(flags)); - } - - // For mono/stereo, 16-bit/32-bit float - taking absolute max value - float peak = 0.f; - if (!(flags & AUDCLNT_BUFFERFLAGS_SILENT) && data != nullptr && mix_format_ != nullptr) - { - WORD tag = mix_format_->wFormatTag; - WORD bps = mix_format_->wBitsPerSample; - - if (tag == WAVE_FORMAT_EXTENSIBLE) - { - // Safe cast to WAVEFORMATEXTENSIBLE* but verify size first - if (mix_format_->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) - { - auto wfex = reinterpret_cast(mix_format_); - - // Safely check GUID comparison - if (IsEqualGUID(wfex->SubFormat, KSDATAFORMAT_SUBTYPE_IEEE_FLOAT)) - tag = WAVE_FORMAT_IEEE_FLOAT; - else if (IsEqualGUID(wfex->SubFormat, KSDATAFORMAT_SUBTYPE_PCM)) - tag = WAVE_FORMAT_PCM; - else - { - Log(L"Unknown SubFormat GUID in WAVE_FORMAT_EXTENSIBLE"); - } - - // Use wValidBitsPerSample if non-zero, otherwise use format's bit depth - bps = wfex->Samples.wValidBitsPerSample; - if (bps == 0) - { - bps = wfex->Format.wBitsPerSample; - } - } - else - { - Log(L"WAVE_FORMAT_EXTENSIBLE with invalid cbSize: " + std::to_wstring(mix_format_->cbSize)); - // Fall back to basic format info - tag = WAVE_FORMAT_PCM; // Assume PCM as fallback - bps = mix_format_->wBitsPerSample; - } - } - - // Safely calculate total samples, checking for integer overflow - UINT32 channels = mix_format_->nChannels; - if (channels == 0) - { - Log(L"Invalid channel count: 0"); - channels = 1; // Default to mono - } - - // Check for integer overflow - UINT64 totalSamples64 = static_cast(frames) * static_cast(channels); - if (totalSamples64 > UINT32_MAX) - { - Log(L"Sample count overflow: " + std::to_wstring(totalSamples64)); - break; - } - - UINT32 totalSamples = static_cast(totalSamples64); - - try - { - if (tag == WAVE_FORMAT_IEEE_FLOAT && bps == 32) - { - const float* samples = reinterpret_cast(data); - for (UINT32 i = 0; i < totalSamples; ++i) - { - float val = samples[i]; - if (val < 0) - val = -val; - if (val > peak) - peak = val; - } - } - else if (tag == WAVE_FORMAT_PCM && bps == 16) - { - const int16_t* samples = reinterpret_cast(data); - for (UINT32 i = 0; i < totalSamples; ++i) - { - float val = samples[i] / 32768.0f; - if (val < 0) - val = -val; - if (val > peak) - peak = val; - } - } - else if (tag == WAVE_FORMAT_PCM && bps == 24) - { - // 24-bit PCM is stored as 3 bytes per sample - const uint8_t* p = reinterpret_cast(data); - - // Check if we have enough bytes for all samples (3 bytes per sample) - UINT64 byteCount = static_cast(totalSamples) * 3; - if (byteCount > SIZE_MAX) - { - Log(L"Byte count overflow for 24-bit audio"); - break; - } - - for (UINT32 i = 0; i < totalSamples; ++i) - { - // Load 3 bytes into 32-bit integer (sign-extended) - int32_t sample = (p[3 * i] << 8) | (p[3 * i + 1] << 16) | (p[3 * i + 2] << 24); - sample >>= 8; // Shift back to get correct sign extension - - // Convert to float -1.0 to 1.0 (normalize by 2^23) - float val = sample / 8388608.0f; - if (val < 0) - val = -val; - if (val > peak) - peak = val; - } - } - else if (tag == WAVE_FORMAT_PCM && bps == 32) - { - const int32_t* samples = reinterpret_cast(data); - for (UINT32 i = 0; i < totalSamples; ++i) - { - // Normalize by 2^31 - float val = samples[i] / 2147483648.0f; - if (val < 0) - val = -val; - if (val > peak) - peak = val; - } - } - else - { - // Unsupported format - Log(L"Unsupported audio format: tag=" + std::to_wstring(tag) + L", bps=" + - std::to_wstring(bps)); - } - } - catch (...) - { - Log(L"Exception during audio processing"); - } - - // Temporary logging to verify data is coming in - Log(L"PollMicrophone: peak=" + std::to_wstring(peak) + L", format tag=" + std::to_wstring(tag) + - L", bps=" + std::to_wstring(bps)); - } - - // For better visualization - slight level smoothing (to avoid sharp jumps) - // In a real implementation this could be a more complex algorithm - float current_level = mic_level_.load(); - float smoothed_level = current_level * 0.5f + peak * 0.5f; // More responsive smoothing - mic_level_.store(smoothed_level); - - // Check if thread should exit before continuing - if (!audio_thread_running_.load()) - { - Log(L"PollMicrophone: Thread signaled to exit, breaking"); - break; - } - - hr = capture_client_->ReleaseBuffer(frames); - if (FAILED(hr)) - { - Log(L"PollMicrophone: ReleaseBuffer failed, hr=0x" + std::to_wstring(hr)); - break; - } - - // Check if thread should exit before getting next packet - if (!audio_thread_running_.load()) - { - Log(L"PollMicrophone: Thread signaled to exit before next packet"); - break; - } - - hr = capture_client_->GetNextPacketSize(&pkt_len); - if (FAILED(hr)) - { - Log(L"PollMicrophone: GetNextPacketSize failed after processing, hr=0x" + std::to_wstring(hr)); - break; - } - } - } - catch (const std::exception& ex) - { - Log(L"PollMicrophone exception: " + std::wstring(ex.what(), ex.what() + strlen(ex.what()))); - } - catch (...) - { - Log(L"PollMicrophone: unknown exception"); - } -} +// Audio capture (WASAPI pipeline, thread, SEH wrapper) lives in void ArgumentDebuggerWindow::Cleanup() { Log(L"Cleanup started"); - // Stop audio first if still running - if (audio_client_) - { - try - { - audio_client_->Stop(); - Log(L"Audio client stopped in cleanup"); - } - catch (...) - { - Log(L"Exception stopping audio client in cleanup"); - } - } - - // Reset audio COM objects first - capture_client_.Reset(); - audio_client_.Reset(); - capture_device_.Reset(); - device_enumerator_.Reset(); + // The audio pipeline is owned by `audio_capture_` and released either by + // its Stop() in OnDestroy or by its destructor. // Reset all graphics ComPtr objects to automatically release resources. vertex_shader_.Reset(); @@ -1590,13 +1133,6 @@ void ArgumentDebuggerWindow::Cleanup() green_brush_.Reset(); yellow_brush_.Reset(); - // Audio thread and event are already closed in OnDestroy - if (mix_format_) - { - CoTaskMemFree(mix_format_); - mix_format_ = nullptr; - } - Log(L"Cleanup finished"); } diff --git a/seh_wrapper.cpp b/seh_wrapper.cpp index 572c2bf..42ab1d8 100644 --- a/seh_wrapper.cpp +++ b/seh_wrapper.cpp @@ -1,56 +1,55 @@ // seh_wrapper.cpp -#include // For swprintf_s +// Turns SEH faults in the audio capture thread (e.g. device disconnect during +// GetBuffer) into a logged termination, instead of letting them propagate as +// an unhandled exception and crashing the process. +// +// Deliberately kept in its own translation unit: /EHsc does not let a C++ +// function that uses __try / __except also have a destructor-bearing local, +// so we isolate the SEH boundary here. + +#include #include -// Forward declaration of the class with explicit method we need to access -class ArgumentDebuggerWindow +// Forward declare just the interface we call — we do not need the full +// AudioCapture definition here, only the `this` pointer plus one method. +class AudioCapture { public: - // Only declare the method we're calling from here - DWORD AudioCaptureThreadImpl(LPVOID param); + DWORD ThreadMain(); }; -// Log function declarations - simplified version for SEH context +// Log function exported from log_manager.cpp (see log_manager.hpp). extern void LogSEH(const wchar_t* message); -// Note: WINAPI is defined as __stdcall for compatibility with Windows API types extern "C" DWORD WINAPI RawAudioThreadWithSEH(LPVOID param) noexcept { DWORD exitCode = 0; __try { - // Check if param is valid if (!param) - { return 0xC0000005; // EXCEPTION_ACCESS_VIOLATION - } - - // Forward to the real implementation - // Note: This expects param to point to an object with AudioCaptureThreadImpl method - ArgumentDebuggerWindow* self = static_cast(param); - exitCode = self->AudioCaptureThreadImpl(param); + + auto* self = static_cast(param); + exitCode = self->ThreadMain(); } __except (EXCEPTION_EXECUTE_HANDLER) { DWORD code = GetExceptionCode(); - // Log through OutputDebugString to avoid std::wstring wchar_t buf[128]; - if (code == EXCEPTION_ACCESS_VIOLATION) { OutputDebugStringW(L"SEH: Access violation in audio thread (0xC0000005)\n"); LogSEH(L"SEH: Access violation in audio thread (0xC0000005)"); - exitCode = 0xC0000005; // EXCEPTION_ACCESS_VIOLATION + exitCode = 0xC0000005; } else if (code == EXCEPTION_STACK_OVERFLOW) { OutputDebugStringW(L"SEH: Stack overflow in audio thread (0xC00000FD)\n"); LogSEH(L"SEH: Stack overflow in audio thread (0xC00000FD)"); - exitCode = 0xC00000FD; // EXCEPTION_STACK_OVERFLOW + exitCode = 0xC00000FD; } else { - // Use wsprintfW instead of swprintf_s for better backward compatibility wsprintfW(buf, L"SEH: Exception in audio thread, code=0x%08X", code); OutputDebugStringW(buf); OutputDebugStringW(L"\n"); @@ -59,4 +58,4 @@ extern "C" DWORD WINAPI RawAudioThreadWithSEH(LPVOID param) noexcept } } return exitCode; -} \ No newline at end of file +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index db11ddc..9d1d0a6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,7 @@ set(TEST_SOURCES set(PARENT_SOURCES ../qrcodegen.cpp ../cli_args_debugger.cpp + ../audio_capture.cpp ../log_manager.cpp ../path_info.cpp ../seh_wrapper.cpp diff --git a/validate.sh b/validate.sh index 10b9bdd..5d47adc 100755 --- a/validate.sh +++ b/validate.sh @@ -19,7 +19,7 @@ echo "🔍 Checking for common C++ syntax issues..." # Check for missing semicolons, unmatched braces, etc. syntax_errors=0 -for file in cli_args_debugger.cpp seh_wrapper.cpp log_manager.cpp path_info.cpp; do +for file in cli_args_debugger.cpp seh_wrapper.cpp log_manager.cpp path_info.cpp audio_capture.cpp; do if [ -f "$file" ]; then echo "Checking $file..."