Skip to content

refactor: extract WASAPI capture into AudioCapture class#6

Merged
kryuchenko merged 1 commit into
mainfrom
refactor/extract-audio-capture
Apr 18, 2026
Merged

refactor: extract WASAPI capture into AudioCapture class#6
kryuchenko merged 1 commit into
mainfrom
refactor/extract-audio-capture

Conversation

@kryuchenko
Copy link
Copy Markdown
Owner

Summary

Step 3 of the cli_args_debugger.cpp decomposition (steps 1 and 2 were #3 and #4). Lifts the microphone pipeline out of ArgumentDebuggerWindow into its own class in audio_capture.{hpp,cpp}.

What moves:

  • 11 data members: 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 audio-specific hunks of OnDestroy and Cleanup.

What consumers see:

  • 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 level meter in RenderFrame.
  • seh_wrapper.cpp now casts the LPVOID parameter to AudioCapture* and calls ThreadMain(). The ArgumentDebuggerWindow forward declaration in that file is gone.

Split of the CCN-50 monster:
PollMicrophone was the worst function in the codebase (CCN 50, 206 NLOC). It is split into:

  • ResolveFormat — handles WAVEFORMATEXTENSIBLE vs plain WAVEFORMATEX (CCN 6).
  • Four single-format peak helpers PeakFloat32, PeakPcm16, PeakPcm24, PeakPcm32 (all CCN 5).
  • PeakForFormat — dispatch (CCN 9).
  • PollOnce — sequences GetNextPacketSizeGetBuffer → peak → ReleaseBuffer (CCN 16, still the highest in the new file, but 3× lower than before).

Numbers:

  • cli_args_debugger.cpp: 1846 → 1382 lines (−464).
  • After this PR only RenderFrame (CCN 21) remains over the static-analysis warning threshold in the main file; step 4 will address it.

Test plan

  • build / windows-build: raw-cl build with new compilation unit.
  • test (CTest): all existing tests (including audio_tests.cpp and seh_exception_tests.cpp) compile and pass against the new class.
  • Memory Safety Tests (ASan Debug): confirms no new leaks or use-after-free on the audio thread's cooperative shutdown path.

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.
@kryuchenko kryuchenko merged commit 5e7793e into main Apr 18, 2026
4 checks passed
@kryuchenko kryuchenko deleted the refactor/extract-audio-capture branch April 18, 2026 19:07
kryuchenko pushed a commit that referenced this pull request Apr 18, 2026
Step 4 (final) of the cli_args_debugger.cpp decomposition (steps 1-3
were #3, #4, #6). RenderFrame was the last function above the static
analysis threshold at CCN 21 / 203 NLOC / 289 lines and touching 37
other class members.

Split into an orchestrator plus ten private section methods:

  UpdateFrameTiming    - tick + FPS + rotation
  RenderCube           - D3D11: clear, viewport, MVP, draw
  RenderTextHud        - description, cli-args header, args, status
  RenderLoadedDataPanel - logs/saved-data panel (show_logs_)
  RenderPathsPanel     - path items (show_paths_)
  RenderInputPrompt    - exit prompt + user_input_
  RenderQrBitmap       - QR code blit
  RenderVolumeMeter    - mic level bars + device name (or no-mic text)
  EndOverlay           - D2D EndDraw + device-lost recreate
  PresentFrame         - FPS log + Present + device-removed recreate

RenderFrame itself is now ~20 lines of sequencing. No behaviour change
intended — each helper receives the same D2D1_SIZE_F and draws into the
same render target, in the same order as before. EndOverlay's bool
return lets the orchestrator skip Present if the D2D device was lost,
mirroring the original early `return`.

Numbers:
- Warnings (CCN > 15 or length > 1000) in cli_args_debugger.cpp:
  was 2 (PollMicrophone=50 already extracted, RenderFrame=21) → now 0.
- cli_args_debugger.cpp stays within a line of the previous total —
  structural split, not a net-line reduction.
- Highest-CCN function in the file drops to WindowProc at CCN 7.
kryuchenko added a commit that referenced this pull request Apr 18, 2026
Step 4 (final) of the cli_args_debugger.cpp decomposition (steps 1-3
were #3, #4, #6). RenderFrame was the last function above the static
analysis threshold at CCN 21 / 203 NLOC / 289 lines and touching 37
other class members.

Split into an orchestrator plus ten private section methods:

  UpdateFrameTiming    - tick + FPS + rotation
  RenderCube           - D3D11: clear, viewport, MVP, draw
  RenderTextHud        - description, cli-args header, args, status
  RenderLoadedDataPanel - logs/saved-data panel (show_logs_)
  RenderPathsPanel     - path items (show_paths_)
  RenderInputPrompt    - exit prompt + user_input_
  RenderQrBitmap       - QR code blit
  RenderVolumeMeter    - mic level bars + device name (or no-mic text)
  EndOverlay           - D2D EndDraw + device-lost recreate
  PresentFrame         - FPS log + Present + device-removed recreate

RenderFrame itself is now ~20 lines of sequencing. No behaviour change
intended — each helper receives the same D2D1_SIZE_F and draws into the
same render target, in the same order as before. EndOverlay's bool
return lets the orchestrator skip Present if the D2D device was lost,
mirroring the original early `return`.

Numbers:
- Warnings (CCN > 15 or length > 1000) in cli_args_debugger.cpp:
  was 2 (PollMicrophone=50 already extracted, RenderFrame=21) → now 0.
- cli_args_debugger.cpp stays within a line of the previous total —
  structural split, not a net-line reduction.
- Highest-CCN function in the file drops to WindowProc at CCN 7.

Co-authored-by: Andrey Kryuchenko <andrey@example.com>
kryuchenko pushed a commit that referenced this pull request Apr 18, 2026
…ests

Splits the 2101-line monolithic cli_args_debugger.cpp into a thin window
class plus four composable modules, and fills the test gap along the way.
End state: cli_args_debugger.cpp down to ~1270 lines, zero functions over
the static-analysis CCN threshold, and 33 new unit tests covering the
extracted pure logic.

Modules extracted
-----------------

log_manager.{hpp,cpp}
  Owns g_log_file / g_logPath, InitLogger / Log / LogSEH / CloseLogger.
  Collapses the duplicated cleanup that was inlined in both branches of
  wWinMain. Keeps the extern globals so tests/logging_tests.cpp links
  unchanged.

path_info.{hpp,cpp}  (namespace path_info)
  Hosts ExecutablePath / CurrentWorkingDirectory / TempDirectory /
  WindowsDirectory / SystemDirectory / OsVersionString /
  WineOrProtonVersion / SaveFilePath / Collect. While moving the code:
    - MAX_PATH truncation fixed in five Win32 helpers via growing-retry
      / queried-size patterns.
    - C-style casts on GetProcAddress → reinterpret_cast.
    - exeDir = exeDir.substr(0, slash) → in-place erase.
    - Wrapped under a namespace so Windows.h macros like
      GetWindowsDirectory don't rewrite the function names.

audio_capture.{hpp,cpp}  (class AudioCapture)
  Pulls the entire WASAPI pipeline out of ArgumentDebuggerWindow: the
  11 audio data members, InitializeMicrophone, PollMicrophone (CCN 50,
  the worst function in the codebase), the capture thread entry, and
  the audio slice of OnDestroy/Cleanup. The seh_wrapper TU now casts
  its LPVOID straight to AudioCapture* and calls ThreadMain, so the
  main class no longer leaks into it. PollMicrophone is split into
  ResolveFormat + four per-format peak helpers + a dispatch; highest
  resulting CCN is 16.

RenderFrame split in place
  The CCN-21, 289-line RenderFrame is replaced by a ~20-line orchestrator
  plus ten private section methods (UpdateFrameTiming, RenderCube,
  RenderTextHud, RenderLoadedDataPanel, RenderPathsPanel,
  RenderInputPrompt, RenderQrBitmap, RenderVolumeMeter, EndOverlay,
  PresentFrame). No behaviour change — each helper draws the same
  region in the same order; EndOverlay's bool return mirrors the
  original device-lost early return.

Build system
------------
CMAKE_CXX_STANDARD 20 (required, extensions off) is set in both
CMakeLists.txt files, aligning the CMake/CTest builds with the
/std:c++20 flag already used by the raw-cl path in build.yml. Prior to
this, CMakeLists.txt left the standard unset (MSVC default ≈ C++14)
and tests/CMakeLists.txt pinned C++17, which is why
std::wstring::data() appeared to work for the main exe but broke
CTest compilation.

Tests
-----
tests/audio_peak_tests.cpp — 22 GoogleTest cases covering PeakFloat32,
PeakPcm16, PeakPcm24, PeakPcm32, PeakForFormat dispatch, ResolveFormat
for plain WAVEFORMATEX and WAVEFORMATEXTENSIBLE unwrapping. Includes
the 24-bit sign-extension edge case that used to be buried in
PollMicrophone, the "unsupported format degrades to 0" contract, and
the zero-channel-count fallback.

tests/path_info_tests.cpp — 11 smoke tests covering every path_info::
helper plus Collect(). Validates the growing-buffer / queried-size
patterns don't truncate, and pins Collect()'s label order so a future
accidental reorder surfaces as a test failure.

To keep these helpers testable without a live microphone, the
anonymous-namespace peak helpers in audio_capture.cpp are promoted
into audio_capture::detail.

Numbers
-------
                         before   after
cli_args_debugger.cpp    2101    ~1270   (-40 %)
functions with CCN > 15     2       0
average CCN (main file)   6.4     3.4
highest CCN (main file)    50       7   (WindowProc)
test suites                12      14

Squash of PRs #3, #4, #5, #6, #7, #8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant