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
85 changes: 45 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,49 +152,51 @@ 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 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 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 | | | | | | | | | |
| Logitech ASTRO A50 Gen 5 | All | x | x | | x | | x | | | | | x | | | | | | x |
| Corsair Headset Device | All | x | x | x | x | | | | | | | | | | | | | |
| Corsair Wireless V2 Headset Device | All | 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 | |
| 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 Buds | 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 3 | All | x | | | | | | | | | | | | | | | | |
| HyperX Cloud II Wireless (Kingston) | All | x | x | | | 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 | | | |
| Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | |
| Sony INZONE H5 | All | x | x | | | | x | | | | | | | x | | | | |
| HeadsetControl Test device | All | 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 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 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 | | | | | | | | | | |
| Logitech ASTRO A50 Gen 5 | All | x | x | | x | | x | | | | | x | | | | | | x | |
| Corsair Headset Device | All | x | x | x | x | | | | | | | | | | | | | | |
| Corsair Wireless V2 Headset Device | All | 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 Buds | 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 3 | All | x | | | | | | | | | | | | | | | | | |
| HyperX Cloud II Wireless (Kingston) | All | x | x | | | 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 | | | | |
| Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | | |
| Sony INZONE H5 | All | x | x | | | | x | | | | | | | x | | | | | |
| HeadsetControl Test device | All | 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

\* Sidetone status reading is currently 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.

## Usage
Expand All @@ -212,6 +214,9 @@ headsetcontrol -b
# Set sidetone level (0-128)
headsetcontrol -s 64

# Read the current sidetone level (when supported)
headsetcontrol -s

# Turn off LEDs
headsetcontrol -l 0

Expand Down
34 changes: 26 additions & 8 deletions cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <format>
#include <iostream>
Expand Down Expand Up @@ -151,8 +151,9 @@ struct Options {
std::optional<uint8_t> noise_filter;

// Info requests
bool request_battery = false;
bool request_chatmix = false;
bool request_battery = false;
bool request_chatmix = false;
bool request_sidetone = false;

// Complex settings
std::optional<EqualizerSettings> equalizer;
Expand Down Expand Up @@ -204,7 +205,21 @@ std::optional<cli::ParseError> configureParser(cli::ArgumentParser& parser, Opti
.long_flag("version", opts.show_version, "Show version information")

// === Feature Controls ===
.value('s', "sidetone", opts.sidetone_level, uint8_t(0), uint8_t(128), "Set sidetone level", "LEVEL")
.custom('s', "sidetone", cli::ArgRequirement::Optional, [&opts](std::optional<std::string_view> arg) -> std::optional<cli::ParseError> {
if (!arg || arg->empty()) {
opts.sidetone_level.reset();
opts.request_sidetone = true;
return std::nullopt;
}

uint8_t level {};
if (auto error = cli::parseInteger(*arg, level, uint8_t(0), uint8_t(128), "sidetone")) {
return error;
}

opts.sidetone_level = level;
opts.request_sidetone = false;
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)")
.toggle('v', "voice-prompt", opts.voice_prompts_enabled, "Turn voice prompts off (0) or on (1)")
Expand Down Expand Up @@ -420,7 +435,7 @@ class HIDEnumeration {
if (devices_)
hid_free_enumeration(devices_);
}
HIDEnumeration(const HIDEnumeration&) = delete;
HIDEnumeration(const HIDEnumeration&) = delete;
HIDEnumeration& operator=(const HIDEnumeration&) = delete;

hid_device_info* get() const { return devices_; }
Expand Down Expand Up @@ -513,7 +528,9 @@ FeatureResult convertToFeatureResult(const headsetcontrol::FeatureOutput& output

// Handle sidetone special case
if (output.sidetone) {
result.value = output.sidetone->current_level;
result.value = output.sidetone->current_level;
result.sidetone_device_level = output.sidetone->device_level;
result.sidetone_level_name = output.sidetone->level_name;
}

return result;
Expand Down Expand Up @@ -791,7 +808,7 @@ namespace help {
// Audio - value hints from capability descriptors
sections.push_back({ "AUDIO", {} });
sections.back()
.add('s', "sidetone", getValueHint(CAP_SIDETONE), "Mic feedback level (0=off)", CAP_SIDETONE)
.add('s', "sidetone", "[LEVEL]", "Get current level or set mic feedback (0-128)", CAP_SIDETONE)
.add("volume-limiter", getValueHint(CAP_VOLUME_LIMITER), "Enable/disable volume limiter", CAP_VOLUME_LIMITER);

// Equalizer
Expand Down Expand Up @@ -974,6 +991,7 @@ void initializeFeatureRequests(std::vector<DiscoveredDevice>& devices, const Opt
{ CAP_BATTERY_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_battery, {} },
{ CAP_INACTIVE_TIME, CAPABILITYTYPE_ACTION, g_feature_params.inactive_time_val, opts.inactive_time.has_value(), {} },
{ CAP_CHATMIX_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_chatmix, {} },
{ CAP_SIDETONE_STATUS, CAPABILITYTYPE_INFO, std::monostate {}, opts.request_sidetone, {} },
{ CAP_VOICE_PROMPTS, CAPABILITYTYPE_ACTION, g_feature_params.voice_prompts_val, opts.voice_prompts_enabled.has_value(), {} },
{ CAP_ROTATE_TO_MUTE, CAPABILITYTYPE_ACTION, g_feature_params.rotate_to_mute_val, opts.rotate_to_mute_enabled.has_value(), {} },
{ CAP_EQUALIZER_PRESET, CAPABILITYTYPE_ACTION, g_feature_params.equalizer_preset_val, opts.equalizer_preset.has_value(), {} },
Expand Down Expand Up @@ -1003,7 +1021,7 @@ void setupSignalHandler()
#ifdef _WIN32
signal(SIGINT, signalHandler);
#else
struct sigaction act {};
struct sigaction act { };
act.sa_handler = signalHandler;
sigaction(SIGINT, &act, nullptr);
#endif
Expand Down
45 changes: 42 additions & 3 deletions cli/output/output.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ using namespace headsetcontrol::serializers;
// Constants
// ============================================================================

constexpr std::string_view API_VERSION = "1.4";
constexpr std::string_view API_VERSION = "1.5";
constexpr std::string_view APP_NAME = "HeadsetControl";

// ============================================================================
Expand Down Expand Up @@ -71,6 +71,20 @@ void processChatmixResult(const FeatureResult& result, DeviceData& dev)
}
}

void processSidetoneResult(const FeatureResult& result, DeviceData& dev)
{
if (result.status == FEATURE_SUCCESS || result.status == FEATURE_INFO) {
dev.sidetone = SidetoneData {
.level = result.value,
.device_level = result.sidetone_device_level.value_or(0),
.name = result.sidetone_level_name.value_or("")
};
} else if (result.status == FEATURE_ERROR) {
dev.errors.emplace_back(capability_to_string(CAP_SIDETONE_STATUS), result.message);
dev.status = STATUS_PARTIAL;
}
}

// Process action capability result and add to device actions
void processActionResult(const FeatureRequest& req, DeviceData& dev, std::string_view device_name)
{
Expand Down Expand Up @@ -99,6 +113,8 @@ void processFeatureRequest(const FeatureRequest& req, DeviceData& dev, std::stri
processBatteryResult(req.result, dev);
} else if (req.cap == CAP_CHATMIX_STATUS) {
processChatmixResult(req.result, dev);
} else if (req.cap == CAP_SIDETONE_STATUS) {
processSidetoneResult(req.result, dev);
} else if (req.type == CAPABILITYTYPE_ACTION) {
processActionResult(req, dev, device_name);
}
Expand Down Expand Up @@ -179,8 +195,7 @@ void processFeatureRequest(const FeatureRequest& req, DeviceData& dev, std::stri
for (const auto& preset : presets->presets) {
preset_data.push_back(EqualizerPresetData {
.name = preset.name,
.values = preset.values
});
.values = preset.values });
}
dev.equalizer_presets = std::move(preset_data);
}
Expand Down Expand Up @@ -293,6 +308,10 @@ void outputYaml(const OutputData& data)
s.write("chatmix", *dev.chatmix);
}

if (dev.sidetone.has_value()) {
dev.sidetone->serialize(s);
}

if (!dev.errors.empty()) {
s.beginObject("errors");
for (const auto& err : dev.errors) {
Expand Down Expand Up @@ -404,6 +423,14 @@ void outputEnv(const OutputData& data)
s.write(prefix + "_CHATMIX", *dev.chatmix);
}

if (dev.sidetone.has_value()) {
s.write(prefix + "_SIDETONE_LEVEL", dev.sidetone->level);
s.write(prefix + "_SIDETONE_DEVICE_LEVEL", dev.sidetone->device_level);
if (!dev.sidetone->name.empty()) {
s.write(prefix + "_SIDETONE_NAME", dev.sidetone->name);
}
}

s.write(prefix + "_ERROR_COUNT", static_cast<int>(dev.errors.size()));
for (size_t j = 0; j < dev.errors.size(); ++j) {
s.write(std::format("{}_ERROR_{}_SOURCE", prefix, j + 1), dev.errors[j].source);
Expand Down Expand Up @@ -471,6 +498,16 @@ void outputStandard(const OutputData& data, bool print_capabilities)
outputted = true;
}

if (dev.sidetone.has_value()) {
if (dev.sidetone->name.empty()) {
s.println("Sidetone: {} (device level {})", dev.sidetone->level, dev.sidetone->device_level);
} else {
s.println("Sidetone: {} ({}; device level {})", dev.sidetone->name,
dev.sidetone->level, dev.sidetone->device_level);
}
outputted = true;
}

for (const auto& err : dev.errors) {
s.println("Error: [{}] {}", err.source, err.message);
outputted = true;
Expand Down Expand Up @@ -545,6 +582,8 @@ void outputShort(const OutputData& data, bool print_capabilities)
}
} else if (dev.chatmix.has_value()) {
s.printValue(*dev.chatmix);
} else if (dev.sidetone.has_value()) {
s.printValue(dev.sidetone->level);
}
}

Expand Down
22 changes: 22 additions & 0 deletions cli/output/output_data.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ struct ActionData {
}
};

struct SidetoneData {
int level = 0;
int device_level = 0;
std::string name;

void serialize(Serializer& s) const
{
s.beginObject("sidetone");
s.write("level", level);
s.write("device_level", device_level);
if (!name.empty()) {
s.write("name", name);
}
s.endObject();
}
};

struct ErrorData {
std::string source;
std::string message;
Expand Down Expand Up @@ -178,6 +195,7 @@ struct DeviceData {

std::optional<BatteryData> battery;
std::optional<int> chatmix;
std::optional<SidetoneData> sidetone;
std::optional<EqualizerData> equalizer;
std::optional<int> equalizer_presets_count;
std::optional<std::vector<EqualizerPresetData>> equalizer_presets;
Expand Down Expand Up @@ -233,6 +251,10 @@ struct DeviceData {
s.write("chatmix", *chatmix);
}

if (sidetone.has_value()) {
sidetone->serialize(s);
}

if (!errors.empty()) {
s.beginObject("errors");
for (const auto& err : errors) {
Expand Down
Loading