Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,10 @@ Capabilities are enumerated in `lib/device.hpp` via the `CAPABILITIES_XLIST` mac
- `CAP_BT_WHEN_POWERED_ON` — Bluetooth-on-power-on behavior
- `CAP_BT_CALL_VOLUME`
- `CAP_NOISE_FILTER`
- `CAP_SIDETONE_STATUS` — Read the current sidetone level
- `CAP_LIGHT_COLOR` — Set the light color (`LightColorSettings`), implies lights on

When adding a capability, update both `CAPABILITIES_XLIST` and the descriptor/handler tables (see Data-Driven Feature System below).
When adding a capability, append it at the end of `CAPABILITIES_XLIST` (never mid-list — the values are C ABI), mirror it in `hsc_capability_t` in `lib/headsetcontrol_c.h`, and update the descriptor/handler tables (see Data-Driven Feature System below).

### Data-Driven Feature System

Expand Down
100 changes: 54 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A cross-platform tool to control USB gaming headsets on **Linux**, **macOS**, an

- **Sidetone** - Hear your own voice without latency (unlike software loopback)
- **Battery Status** - Monitor charge level, voltage, and time remaining
- **LED Control** - Toggle lights on/off
- **LED Control** - Toggle lights on/off and set their color
- **Equalizer** - Presets and custom EQ curves (including parametric EQ)
- **Inactive Time** - Auto power-off timer
- **Chat-Mix** - Game/chat audio balance
Expand Down Expand Up @@ -152,56 +152,58 @@ sudo udevadm control --reload-rules && sudo udevadm trigger

## Supported Devices

| Device | Platform | sidetone | battery | notification sound | lights | inactive time | chatmix | voice prompts | rotate to mute | equalizer preset | equalizer | parametric equalizer | microphone mute led brightness | microphone volume | volume limiter | bluetooth when powered on | bluetooth call volume | microphone noise filter | sidetone status |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Logitech ASTRO A50 Gen 5 | All | x | x | | x | | x | | | | | x | | | | | | x | |
| Logitech G522 LIGHTSPEED | All | x | x | | | x | | | | | | | x | | | | | | |
| Logitech G533 | All | x | x | | | x | | | | | | | | | | | | | |
| Logitech G535 | All | x | x | | | x | | | | | | | | | | | | | |
| Logitech G633/G635/G733/G933/G935 | All | x | x | | x | | | | | | | | | | | | | | |
| Logitech G431/G432/G433 | All | x | | | | | | | | | | | | | | | | | |
| Logitech G930 | All | x | x | | | | | | | | | | | | | | | | |
| Logitech G PRO X 2 LIGHTSPEED | All | x | x | | | x | | | | x | x | x | | | | | | | |
| Logitech G PRO Series | All | x | x | | | x | | | | | | | | | | | | | |
| Logitech Zone Wired/Zone 750 | All | x | | | | | | x | x | | | | | | | | | | |
| Corsair Headset Device | All | x | x | x | x | | | | | | | | | | | | | | |
| Corsair Wireless V2 Headset Device | All | x | x | | | x | | | | | | | | | | | | | |
| Corsair Virtuoso XT/SE | All | | x | | | | | | | | | | | | | | | | |
| SteelSeries Arctis (1/7X/7P) Wireless | All | x | x | | | x | | | | | | | | | | | | | |
| SteelSeries Arctis (7/Pro) | All | x | x | | x | x | x | | | | | | | | | | | | |
| SteelSeries Arctis 9 | All | x | x | | | x | x | | | | | | | | | | | | |
| SteelSeries Arctis Pro Wireless | All | x | x | | | x | | | | | | | | | | | | | |
| SteelSeries Arctis Nova 3 | All | x | | | | | | | | x | x | | x | x | | | | | |
| SteelSeries Arctis Nova (5/5X) | All | x | x | | | x | x | | | x | x | x | x | x | x | | | | |
| SteelSeries Arctis Nova 7 | All | x | x | | | x | x | | | x | x | | x | x | x | x | x | | x* |
| SteelSeries Arctis Nova 7P | All | | x | | | x | | | | x | x | | x | x | x | x | x | | |
| SteelSeries Arctis 7+ | All | x | x | | | x | x | | | x | x | | | | | | | | |
| SteelSeries Arctis Nova Pro Wireless | All | x | x | | x | x | | | | x | x | | | | | | | | |
| SteelSeries Arctis Nova 3P Wireless | All | x | x | | | x | | | | x | x | x | | x | | | | | |
| SteelSeries Arctis GameBuds | All | | x | | | | | | | | | | | | | | | | |
| HyperX Cloud Alpha Wireless | All | x | x | | | x | | x | | | | | | | | | | | |
| HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | | |
| HyperX Cloud II Wireless | All | | x | | | x | | | | | | | | | | | | | |
| HyperX Cloud II Wireless (Kingston) | All | x | x | | | x | | | | | | | | | | | | | |
| HyperX Cloud 3 | All | x | | | | | | | | | | | | | | | | | |
| ROCCAT Elo 7.1 Air | All | | | | x | x | | | | | | | | | | | | | |
| ROCCAT Elo 7.1 USB | All | | | | x | | | | | | | | | | | | | | |
| Audeze Maxwell | All | x | x | | | x | x | x | | x | | | | | x | | | x | |
| Audeze Maxwell 2 | All | x | x | | | x | x | x | | x | | | | | | | | x | |
| Lenovo Wireless VoIP Headset | All | x | x | | | x | | x | x | x | | | x | | x | | | | |
| Plantronics Voyager 8200 UC (BT600) | L/W | x | x | | x | | | x | | | | | | | x | | | | |
| Jabra Link 390 (paired headset) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x |
| Jabra Evolve2 65 Flex (USB) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x |
| Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | | |
| Sony INZONE H5 | All | x | x | | | | x | | | | | | | x | | | | | |
| MCHOSE X9 Wireless | L/W | | x | | | | | | | | | | | | | | | | |
| HeadsetControl Test device | All | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |
| Device | Platform | sidetone | battery | notification sound | lights | inactive time | chatmix | voice prompts | rotate to mute | equalizer preset | equalizer | parametric equalizer | microphone mute led brightness | microphone volume | volume limiter | bluetooth when powered on | bluetooth call volume | microphone noise filter | sidetone status | light color |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Logitech ASTRO A50 Gen 5 | All | x | x | | x | | x | | | | | x | | | | | | x | | |
| Logitech G522 LIGHTSPEED | All | x | x | | | x | | | | | | | x | | | | | | | |
| Logitech G533 | All | x | x | | | x | | | | | | | | | | | | | | |
| Logitech G535 | All | x | x | | | x | | | | | | | | | | | | | | |
| Logitech G633/G635/G733/G933/G935 | All | x | x | | x | | | | | | | | | | | | | | | |
| Logitech G431/G432/G433 | All | x | | | | | | | | | | | | | | | | | | |
| Logitech G930 | All | x | x | | | | | | | | | | | | | | | | | |
| Logitech G PRO X 2 LIGHTSPEED | All | x | x | | | x | | | | x | x | x | | | | | | | | |
| Logitech G PRO Series | All | x | x | | | x | | | | | | | | | | | | | | |
| Logitech Zone Wired/Zone 750 | All | x | | | | | | x | x | | | | | | | | | | | |
| Corsair Headset Device | All | x | x | x | x | | | | | | | | | | | | | | | |
| Corsair Wireless V2 Headset Device | All | x | x | | | x | | | | | | | | | | | | | | |
| Corsair Virtuoso XT/SE | All | x | x | | x | x | | | | | | | | | | | | | | x |
| SteelSeries Arctis (1/7X/7P) Wireless | All | x | x | | | x | | | | | | | | | | | | | | |
| SteelSeries Arctis (7/Pro) | All | x | x | | x | x | x | | | | | | | | | | | | | |
| SteelSeries Arctis 9 | All | x | x | | | x | x | | | | | | | | | | | | | |
| SteelSeries Arctis Pro Wireless | All | x | x | | | x | | | | | | | | | | | | | | |
| SteelSeries Arctis Nova 3 | All | x | | | | | | | | x | x | | x | x | | | | | | |
| SteelSeries Arctis Nova (5/5X) | All | x | x | | | x | x | | | x | x | x | x | x | x | | | | | |
| SteelSeries Arctis Nova 7 | All | x | x | | | x | x | | | x | x | | x | x | x | x | x | | x* | |
| SteelSeries Arctis Nova 7P | All | | x | | | x | | | | x | x | | x | x | x | x | x | | | |
| SteelSeries Arctis 7+ | All | x | x | | | x | x | | | x | x | | | | | | | | | |
| SteelSeries Arctis Nova Pro Wireless | All | x | x | | x | x | | | | x | x | | | | | | | | | |
| SteelSeries Arctis Nova 3P Wireless | All | x | x | | | x | | | | x | x | x | | x | | | | | | |
| SteelSeries Arctis GameBuds | All | | x | | | | | | | | | | | | | | | | | |
| HyperX Cloud Alpha Wireless | All | x | x | | | x | | x | | | | | | | | | | | | |
| HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | | | |
| HyperX Cloud II Wireless | All | | x | | | x | | | | | | | | | | | | | | |
| HyperX Cloud II Wireless (Kingston) | All | x | x | | | x | | | | | | | | | | | | | | |
| HyperX Cloud 3 | All | x | | | | | | | | | | | | | | | | | | |
| ROCCAT Elo 7.1 Air | All | | | | x | x | | | | | | | | | | | | | | |
| ROCCAT Elo 7.1 USB | All | | | | x | | | | | | | | | | | | | | | |
| Audeze Maxwell | All | x | x | | | x | x | x | | x | | | | | x | | | x | | |
| Audeze Maxwell 2 | All | x | x | | | x | x | x | | x | | | | | | | | x | | |
| Lenovo Wireless VoIP Headset | All | x | x | | | x | | x | x | x | | | x | | x | | | | | |
| Plantronics Voyager 8200 UC (BT600) | L/W | x | x | | x | | | x | | | | | | | x | | | | | |
| Jabra Link 390 (paired headset) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x | |
| Jabra Evolve2 65 Flex (USB) | L/M | x | x | | x | x | | x | | | | | | | x | | | | x | |
| Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | | | |
| Sony INZONE H5 | All | x | x | | | | x | | | | | | | x | | | | | | |
| MCHOSE X9 Wireless | L/W | | x | | | | | | | | | | | | | | | | | |
| HeadsetControl Test device | All | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x |

**Platform:** All = Linux, macOS, Windows | L/M = Linux and macOS only | L/W = Linux and Windows only

\* Only available on some product variants of that device. Sidetone status reading, for instance, is verified only for the SteelSeries Arctis Nova 7 Gen 2 (`1038:227e`).

> **Note:** Some Corsair headsets may need additional configuration - see [Adding a Corsair device](docs/ADDING_A_CORSAIR_DEVICE.md). Some headsets (HS80, HS70 wired, RGB Elite, Virtuoso) expose sidetone via ALSA mixer instead.
> **Note:** Some Corsair headsets may need additional configuration - see [Adding a Corsair device](docs/ADDING_A_CORSAIR_DEVICE.md). Some headsets (HS80, HS70 wired, RGB Elite, Virtuoso other than the XT/SE) expose sidetone via ALSA mixer instead.

> **Note:** On the Corsair Virtuoso XT/SE, `--light-color` is temporary: the headset returns to its own lighting effect about a minute after the last command. `-l 1` brings its own effect back straight away.

## Usage

Expand All @@ -224,6 +226,12 @@ headsetcontrol -s
# Turn off LEDs
headsetcontrol -l 0

# Set the LED color, which also turns them on (applies to every LED zone)
headsetcontrol --light-color ff8000

# -l and --light-color together: the color is applied last, so it wins
headsetcontrol -l 0 --light-color '#00ff00'

# Set auto-off timer (minutes, 0 = disabled)
headsetcontrol -i 30

Expand Down
21 changes: 20 additions & 1 deletion cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ struct Options {
// Complex settings
std::optional<EqualizerSettings> equalizer;
std::optional<ParametricEqualizerSettings> parametric_equalizer;
std::optional<LightColorSettings> light_color;

// Helper
[[nodiscard]] bool hasDeviceFilter() const
Expand Down Expand Up @@ -232,6 +233,15 @@ std::optional<cli::ParseError> configureParser(cli::ArgumentParser& parser, Opti
return std::nullopt; }, "Get current sidetone level, or set it to LEVEL", "LEVEL")
.flag('b', "battery", opts.request_battery, "Check battery level")
.toggle('l', "light", opts.lights_enabled, "Turn lights off (0) or on (1)")
.long_custom("light-color", cli::ArgRequirement::Required, [&opts](std::optional<std::string_view> arg) -> std::optional<cli::ParseError> {
if (!arg)
return cli::ParseError { "requires a color", "light-color" };
auto color = headsetcontrol::parse_light_color(*arg);
if (!color) {
return cli::ParseError { "format: RRGGBB or #RRGGBB", "light-color" };
}
opts.light_color = *color;
return std::nullopt; }, "Set light color, which also turns the lights on", "RRGGBB")
.toggle('v', "voice-prompt", opts.voice_prompts_enabled, "Turn voice prompts off (0) or on (1)")
.value('i', "inactive-time", opts.inactive_time, uint8_t(0), uint8_t(90), "Set inactive time in minutes", "MINUTES")
.flag('m', "chatmix", opts.request_chatmix, "Get chat-mix level")
Expand Down Expand Up @@ -586,6 +596,8 @@ FeatureResult convertToFeatureResult(const headsetcontrol::FeatureOutput& output
result.sidetone_level_name = output.sidetone->level_name;
}

result.light_color = output.light_color;

return result;
}

Expand Down Expand Up @@ -893,6 +905,7 @@ namespace help {
sections.push_back({ "LIGHTS & AUDIO CUES", {} });
sections.back()
.add('l', "light", getValueHint(CAP_LIGHTS), "RGB/LED lights off/on", CAP_LIGHTS)
.add("light-color", getValueHint(CAP_LIGHT_COLOR), "Set light color; turns lights on, and wins over -l", CAP_LIGHT_COLOR)
.add('v', "voice-prompt", getValueHint(CAP_VOICE_PROMPTS), "Voice prompts off/on", CAP_VOICE_PROMPTS)
.add('n', "notificate", getValueHint(CAP_NOTIFICATION_SOUND), "Play notification sound", CAP_NOTIFICATION_SOUND);

Expand Down Expand Up @@ -988,6 +1001,7 @@ struct FeatureParamStorage {
// Store copies of complex settings to avoid const_cast
EqualizerSettings equalizer_settings;
ParametricEqualizerSettings parametric_eq_settings;
LightColorSettings light_color_settings;

void updateFrom(const Options& opts)
{
Expand Down Expand Up @@ -1025,6 +1039,8 @@ struct FeatureParamStorage {
equalizer_settings = *opts.equalizer;
if (opts.parametric_equalizer.has_value())
parametric_eq_settings = *opts.parametric_equalizer;
if (opts.light_color.has_value())
light_color_settings = *opts.light_color;
}
};

Expand Down Expand Up @@ -1055,7 +1071,10 @@ void initializeFeatureRequests(std::vector<DiscoveredDevice>& devices, const Opt
{ CAP_VOLUME_LIMITER, CAPABILITYTYPE_ACTION, g_feature_params.volume_limiter_val, opts.volume_limiter_enabled.has_value(), {} },
{ CAP_BT_WHEN_POWERED_ON, CAPABILITYTYPE_ACTION, g_feature_params.bt_power_val, opts.bt_when_powered_on.has_value(), {} },
{ CAP_BT_CALL_VOLUME, CAPABILITYTYPE_ACTION, g_feature_params.bt_call_vol_val, opts.bt_call_volume.has_value(), {} },
{ CAP_NOISE_FILTER, CAPABILITYTYPE_ACTION, g_feature_params.noise_filter_val, opts.noise_filter.has_value(), {} }
{ CAP_NOISE_FILTER, CAPABILITYTYPE_ACTION, g_feature_params.noise_filter_val, opts.noise_filter.has_value(), {} },
// Last on purpose: requests run in this order, so with both -l and
// --light-color on one command line the color is applied after -l and wins.
{ CAP_LIGHT_COLOR, CAPABILITYTYPE_ACTION, opts.light_color.has_value() ? FeatureParam { g_feature_params.light_color_settings } : FeatureParam { std::monostate {} }, opts.light_color.has_value(), {} }
};

for (auto& dev : devices) {
Expand Down
17 changes: 14 additions & 3 deletions cli/output/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ using namespace headsetcontrol::serializers;
// 1.5: an invocation that performs an action no longer reports info it was not
// explicitly asked for, and structured output gained an additive "sidetone"
// field for devices that support reading it back.
constexpr std::string_view API_VERSION = "1.5";
// 1.6: action entries gained an additive "color" field ("#rrggbb") for
// CAP_LIGHT_COLOR, whose "value" packs 0xRRGGBB and is reported even for black.
constexpr std::string_view API_VERSION = "1.6";
constexpr std::string_view APP_NAME = "HeadsetControl";

// ============================================================================
Expand Down Expand Up @@ -98,6 +100,10 @@ void processActionResult(const FeatureRequest& req, DeviceData& dev, std::string
action.status = req.result.status == FEATURE_SUCCESS ? STATUS_SUCCESS : STATUS_FAILURE;
action.value = req.result.value;
action.error_message = req.result.message;
if (req.result.light_color) {
const auto& c = *req.result.light_color;
action.color = std::format("#{:02x}{:02x}{:02x}", c.r, c.g, c.b);
}
dev.actions.push_back(std::move(action));
}

Expand Down Expand Up @@ -251,8 +257,10 @@ void outputYaml(const OutputData& data)
s.pushIndent(1); // Align subsequent keys with "capability" after "- "
s.write("device", action.device);
s.write("status", statusToString(action.status));
if (action.value > 0)
if (action.hasValue())
s.write("value", action.value);
if (action.color)
s.write("color", *action.color);
if (!action.error_message.empty())
s.write("error_message", action.error_message);
s.popIndent(1);
Expand Down Expand Up @@ -357,9 +365,12 @@ void outputEnv(const OutputData& data)
s.write(prefix + "_CAPABILITY", action.capability);
s.write(prefix + "_DEVICE", action.device);
s.write(prefix + "_STATUS", statusToString(action.status));
if (action.value > 0) {
if (action.hasValue()) {
s.write(prefix + "_VALUE", action.value);
}
if (action.color) {
s.write(prefix + "_COLOR", *action.color);
}
if (!action.error_message.empty()) {
s.write(prefix + "_ERROR_MESSAGE", action.error_message);
}
Expand Down
Loading