This file provides guidance to AI coding assistants (Claude Code, Copilot, Cursor, Codex, etc.) when working with code in this repository.
HeadsetControl is a cross-platform C++20 application to control USB-connected headsets on Linux, macOS, and Windows. It enables controlling features like sidetone, battery status, LEDs, inactive time, equalizers, and more for various gaming headsets (Logitech, SteelSeries, Corsair, HyperX, Roccat, Audeze).
The project uses CMake as its build system.
# Initial setup
mkdir build && cd build
cmake ..
make
# Install globally (includes udev rules on Linux)
sudo make install
# On Linux after install, reload udev rules:
sudo udevadm control --reload-rules && sudo udevadm trigger# Enable + build + run unit tests
cmake -DBUILD_UNIT_TESTS=ON ..
make check # Builds dependencies and runs ctest --output-on-failure
# Or just:
ctestIMPORTANT: The CI uses clang-format version 18. To avoid formatting conflicts, install the matching version:
# On macOS:
brew install llvm@18
# On Linux (Ubuntu/Debian):
sudo apt-get install clang-format-18
# Format all code
cmake -DENABLE_CLANG_FORMAT=ON ..
make formatcmake -DENABLE_CLANG_TIDY=ON ..
make tidyThe tidy target prefers clang-tidy-9 if installed, otherwise falls back to the system clang-tidy (typically the same version as your formatter).
HeadsetControl/
├── lib/ # Core library (libheadsetcontrol)
│ ├── devices/ # Device implementations (header-only)
│ │ ├── hid_device.hpp # Base class for all devices
│ │ ├── protocols/ # CRTP protocol templates
│ │ │ ├── hidpp_protocol.hpp
│ │ │ ├── steelseries_protocol.hpp
│ │ │ ├── logitech_centurion_protocol.hpp
│ │ │ └── logitech_calibrations.hpp
│ │ └── *.hpp # Device-specific implementations
│ ├── device.hpp # Capability enums and structs
│ ├── device_registry.hpp # Device lookup singleton
│ ├── result_types.hpp # Result<T> error handling
│ ├── capability_descriptors.hpp # CAPABILITY_DESCRIPTORS metadata array
│ ├── feature_handlers.hpp # FeatureHandlerRegistry dispatch table
│ ├── headsetcontrol.hpp # Public C++ API
│ ├── headsetcontrol_c.h # Public C API
│ ├── dev.hpp / dev.cpp # Dev-mode HID exploration helpers
│ └── utility.hpp # Helper functions
├── cli/ # Command-line interface
│ ├── main.cpp # Entry point
│ ├── argument_parser.hpp # CLI argument parsing
│ ├── dev.cpp # Developer/debug mode (--dev)
│ └── output/ # Serialization (JSON, YAML, ENV)
├── tests/ # Unit and integration tests
├── docs/ # Documentation (ADDING_A_DEVICE, ADDING_A_CAPABILITY,
│ # ADDING_A_CORSAIR_DEVICE, DEVELOPMENT, LIBRARY_USAGE)
├── cmake_modules/ # Findhidapi.cmake and related
├── assets/ # Icons and Windows resource file
├── .github/ # CI workflows, issue and PR templates
├── CMakePresets.json # CMake presets
└── vcpkg.json # vcpkg manifest (Windows builds)
The core architecture uses a device registry pattern with modern C++20:
-
Device Registry (
lib/device_registry.hpp):- Singleton pattern for device management
DeviceRegistry::instance().initialize()registers all devicesgetDevice(vendor_id, product_id)looks up devices by USB IDsgetAllDevices()returns all registered implementations
-
HIDDevice Base Class (
lib/devices/hid_device.hpp):- Pure virtual interface for all headset devices
- Virtual methods for each capability (e.g.,
setSidetone(),getBattery()) - Returns
Result<T>types for proper error handling - Provides HID communication helpers (
writeHID(),readHIDTimeout())
-
Device Implementations (
lib/devices/*.hpp):- Each headset is a class inheriting from
HIDDevice(or a CRTP protocol template) - Header-only implementations
- CRTP protocol templates under
lib/devices/protocols/reduce boilerplate (HID++, SteelSeries, Logitech Centurion). Inherit via e.g.protocols::HIDPPDevice<MyDevice>.
- Each headset is a class inheriting from
All device methods return Result<T> (similar to std::expected):
Result<BatteryResult> getBattery(hid_device* handle) override {
auto result = readHIDTimeout(handle, buffer, timeout);
if (!result) {
return result.error(); // Propagate error
}
return BatteryResult { .level_percent = 85, .status = BATTERY_AVAILABLE };
}Error types: DeviceError::timeout(), hidError(), protocolError(), notSupported()
See docs/ADDING_A_DEVICE.md for detailed instructions. Quick overview:
- Create
lib/devices/vendor_model.hpp - Inherit from
HIDDeviceor a protocol template - Implement required virtual methods
- Register in
lib/device_registry.cpp
See docs/LIBRARY_USAGE.md for integration guide. The library provides:
headsetcontrol_libstatic library targetheadsetcontrol_sharedshared library target (with-DBUILD_SHARED_LIBRARY=ON)- Public headers in
lib/directory - C++ API (
headsetcontrol.hpp) and C API (headsetcontrol_c.h) - Device discovery and control API
# Build shared library for FFI (Python, Rust, etc.)
cmake -DBUILD_SHARED_LIBRARY=ON ..
makeCapabilities are enumerated in lib/device.hpp via the CAPABILITIES_XLIST macro. The full set:
CAP_SIDETONE— Microphone sidetone levelCAP_BATTERY_STATUS— Battery level and charging statusCAP_NOTIFICATION_SOUND— Play a notification toneCAP_LIGHTS— LED on/offCAP_INACTIVE_TIME— Auto-shutoff timerCAP_CHATMIX_STATUS— Game/chat audio balanceCAP_VOICE_PROMPTS— Toggle spoken promptsCAP_ROTATE_TO_MUTE— Rotate boom mic to muteCAP_EQUALIZER_PRESET— Select preset EQCAP_EQUALIZER— Custom EQ bandsCAP_PARAMETRIC_EQUALIZER— Parametric EQ (gain, Q-factor, frequency)CAP_MICROPHONE_MUTE_LED_BRIGHTNESSCAP_MICROPHONE_VOLUMECAP_VOLUME_LIMITERCAP_BT_WHEN_POWERED_ON— Bluetooth-on-power-on behaviorCAP_BT_CALL_VOLUMECAP_NOISE_FILTER
When adding a capability, update both CAPABILITIES_XLIST and the descriptor/handler tables (see Data-Driven Feature System below).
The codebase uses a data-driven approach for feature handling:
-
Capability Descriptors (
lib/capability_descriptors.hpp):- Single source of truth for all capability metadata
- CLI flag names, descriptions, value ranges
- Used for validation and help text generation
-
Feature Handler Registry (
lib/feature_handlers.hpp):- Dispatch table for feature execution
- Eliminates giant switch statements
- One handler per capability
// Adding a new capability only requires:
// 1. Add descriptor to CAPABILITY_DESCRIPTORS array
// 2. Register handler in FeatureHandlerRegistry::registerAllHandlers()See docs/ADDING_A_CAPABILITY.md for a step-by-step guide.
- C++20 standard with modern features
.clang-formatuses WebKit base style- RAII for resource management
Result<T>is[[nodiscard]]at the class level — you do not need to mark virtual override methods returningResult<T>with[[nodiscard]]; the enforcement is on the typestd::formatfor string formattingstd::optional,std::span,std::string_view- Designated initializers for struct initialization
- Device files are header-only (
.hpp); the only.cppinlib/devices/ishid_device.cpp - Naming: methods
camelCase, member variablessnake_case_(trailing underscore), constantsALL_CAPS using namespace std::string_view_literals;at file scope is the convention in device headers- Use
[[maybe_unused]]on unused parameters in virtual overrides
- HIDAPI — USB HID communication library (required)
- CMake — Build system (minimum 3.12)
- C++20 compiler — GCC 13+, Clang 16+, Apple Clang 15+, MSVC 19.29+ (VS 2019 16.10+) as enforced in
CMakeLists.txt
- Generates udev rules:
headsetcontrol -u > /etc/udev/rules.d/70-headset.rules - Reload rules:
sudo udevadm control --reload-rules && sudo udevadm trigger
- No special permissions needed
- Homebrew:
brew install sapd/headsetcontrol/headsetcontrol --HEAD
- Uses SetupAPI for HID access
- Some devices require
usagepage/usageidinstead ofinterface
- Unit tests:
tests/test_*.cppusing a lightweight test framework - Test device:
HeadsetControlTest(0xF00B:0xA00C) implements all capabilities - Run with:
./headsetcontrol --test-device -b - Dev mode:
./headsetcontrol --dev -- --listfor HID exploration