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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ A cross-platform tool to control USB gaming headsets on **Linux**, **macOS**, an
| 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 | | | |
| 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 | | | |
| Sony INZONE Buds | All | | x | | | | | | | | | | | | | | | |
Expand Down
140 changes: 82 additions & 58 deletions lib/devices/audeze_maxwell.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ namespace headsetcontrol {
* - Battery status
* - Chatmix
* - Voice prompts
* - Mic noise filter
*
* Special Requirements:
* - Complex 18-packet initialization sequence
* - 60ms delay between packets (mimics official software)
* - 15-packet initialization sequence followed by 6 status requests
* - 60ms delay between packets (official software sends at 52ms but 60ms is more reliable in testing)
* - Uses hid_get_input_report for responses
*/
class AudezeMaxwell : public HIDDevice {
Expand All @@ -39,8 +40,8 @@ class AudezeMaxwell : public HIDDevice {
static constexpr int REPORT_ID = 0x06;
static constexpr int DELAY_US = 60000; // 60ms

// 18-packet initialization sequence (12 unique + 5 status + 1 final)
static constexpr std::array<std::array<uint8_t, MSG_SIZE>, 13> UNIQUE_REQUESTS { { { 0x6, 0x8, 0x80, 0x5, 0x5a, 0x4, 0x0, 0x1, 0x9, 0x20 },
// 15-packet initialization sequence
static constexpr std::array<std::array<uint8_t, MSG_SIZE>, 15> UNIQUE_REQUESTS { { { 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x20 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x25 },
{ 0x06, 0x07, 0x80, 0x05, 0x5A, 0x03, 0x00, 0x07, 0x1C },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x28 },
Expand All @@ -51,17 +52,18 @@ class AudezeMaxwell : public HIDDevice {
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x2C },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x83, 0x2C, 0x0B },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x24 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x2F },
{ 0x06, 0x09, 0x80, 0x05, 0x5A, 0x05, 0x00, 0x00, 0x09, 0x25, 0x00, 0x7A },
{ 0x06, 0x07, 0x80, 0x05, 0x5A, 0x03, 0x00, 0xD6, 0x0C } } };

// Status requests (5 packets for battery, sidetone, chatmix info)
static constexpr std::array<std::array<uint8_t, MSG_SIZE>, 5> STATUS_REQUESTS { {
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x22 }, // Battery, Sidetone enabled
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09 }, // Battery, Mic
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x83, 0x2C, 0x0B }, // Battery
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x2C }, // Battery, ChatMix
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x83, 0x2C, 0x07 } // Battery, Sidetone Level
} };
// 6 packet status request sequence
static constexpr std::array<std::array<uint8_t, MSG_SIZE>, 6> STATUS_REQUESTS { { { 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x22 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x83, 0x2C, 0x0B },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x24 },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x01, 0x09, 0x2C },
{ 0x06, 0x08, 0x80, 0x05, 0x5A, 0x04, 0x00, 0x83, 0x2C, 0x07 } } };

constexpr uint16_t getVendorId() const override
{
Expand All @@ -82,7 +84,7 @@ class AudezeMaxwell : public HIDDevice {
{
return B(CAP_SIDETONE) | B(CAP_INACTIVE_TIME) | B(CAP_VOLUME_LIMITER)
| B(CAP_EQUALIZER_PRESET) | B(CAP_BATTERY_STATUS) | B(CAP_CHATMIX_STATUS)
| B(CAP_VOICE_PROMPTS);
| B(CAP_VOICE_PROMPTS) | B(CAP_NOISE_FILTER);
}

constexpr capability_detail getCapabilityDetail([[maybe_unused]] enum capabilities cap) const override
Expand Down Expand Up @@ -129,64 +131,87 @@ class AudezeMaxwell : public HIDDevice {
}

/**
* @brief Get comprehensive device status (battery, chatmix, sidetone)
* @brief Get device status (battery, chatmix, sidetone, mic mute)
*
* Must send all 5 status requests to get consistent information
* Must send all 6 status requests to get consistent information
*/
struct MaxwellStatus {
BatteryResult battery;
int chatmix_level;
int sidetone_level;
int eq_setting;
int ainf_level;
bool sidetone_enabled;
bool mic_muted;
};

Result<MaxwellStatus> getDeviceStatus(hid_device* device_handle) const
{
std::array<std::array<uint8_t, MSG_SIZE>, 5> status_buffs {};
std::array<std::array<uint8_t, MSG_SIZE>, 6> status_buffs {};

for (int i = 0; i < 5; ++i) {
auto result = sendGetInputReport(device_handle, STATUS_REQUESTS[i], status_buffs[i]);
if (!result) {
// Send UNIQUE_REQUESTS packets
for (auto unique_request : UNIQUE_REQUESTS) {
if (auto result = sendGetInputReport(device_handle, unique_request); !result) {
return result.error();
}
}

// Send STATUS_REQUESTS packets
for (int i = 0; i < 6; ++i) {
if (auto result = sendGetInputReport(device_handle, STATUS_REQUESTS[i], status_buffs[i]); !result) {
return result.error();
}
}

// Status_buffs structure
// status_buffs[0] = battery_status
// status_buffs[1] = mic_status
// status_buffs[2] = eq
// status_buffs[3] = chatmix
// status_buffs[4] = ainf_level
// status_buffs[5] = sidetone_level

MaxwellStatus status {};

// Parse battery (from first status response)
const auto& buff = status_buffs[0];
if (buff[1] == 0x00) {

// Initialize battery status as unavailable
status.battery.status = BATTERY_UNAVAILABLE;
status.battery.level_percent = -1;

// Parse battery level
if (buff[0] == 0x00) {
status.battery.status = BATTERY_UNAVAILABLE;
status.battery.level_percent = -1;
} else {
// Find battery level in response
// Find battery level in response (level is first byte after d6 0c 00 00 in packet)
for (int i = 0; i < MSG_SIZE - 4; ++i) {
if (buff[i] == 0xD6 && buff[i + 1] == 0x0C && buff[i + 2] == 0x00 && buff[i + 3] == 0x00) {
status.battery.level_percent = buff[i + 4];
status.battery.status = BATTERY_AVAILABLE;
break;
}
}

// Check microphone status (note: BatteryResult has mic_status field)
if (status_buffs[1][12] == 0xFF) {
status.battery.mic_status = MICROPHONE_UP;
}
}

// Parse microphone mute status (FF = unmuted)
status.mic_muted = status_buffs[1][12] != 0xFF;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could in pass that information btw in BatteryResult back


// Parse current eq setting
// 1 = Audeze, 2 = Treble Boost, 3 = Bass Boost, 4 = Immersive, 5 = Competition,
// 6 = Footsteps, 7 = EQ1, 8 = EQ2, 9 = EQ3, 10 = EQ4
status.eq_setting = status_buffs[2][12];

// Parse chatmix (0-20 range, center at 10)
status.chatmix_level = map(status_buffs[3][12], 0, 20, 0, 128);

// Parse sidetone (0-31 range)
status.sidetone_level = map(status_buffs[4][12], 0, 31, 0, 128);
status.sidetone_enabled = status_buffs[0][12] != 0;
// Parse current mic noise filter level
// 0 = Off, 1 = Low, 2 = High
status.ainf_level = status_buffs[4][12];

// If battery unavailable, run full init sequence
if (status.battery.status != BATTERY_AVAILABLE) {
for (int i = 0; i < 13; ++i) {
(void)sendGetInputReport(device_handle, UNIQUE_REQUESTS[i]);
}
}
// Parse sidetone (0-31 range)
status.sidetone_level = map(status_buffs[5][12], 0, 31, 0, 128);
status.sidetone_enabled = status_buffs[5][12] != 0;

return status;
}
Expand All @@ -208,7 +233,7 @@ class AudezeMaxwell : public HIDDevice {
// Maxwell range: 0 to 31
uint8_t mapped = map<uint8_t>(level, 0, 128, 0, 31);

std::array<uint8_t, MSG_SIZE> cmd { 0x6, 0x9, 0x80, 0x5, 0x5a, 0x5, 0x0, 0x0, 0x9, 0x2c, 0x0, mapped };
std::array<uint8_t, MSG_SIZE> cmd { 0x06, 0x09, 0x80, 0x05, 0x5A, 0x05, 0x00, 0x00, 0x09, 0x2C, 0x00, mapped };

// Sidetone is enabled whenever level changes
auto result = sendGetInputReport(device_handle, cmd);
Expand All @@ -218,7 +243,7 @@ class AudezeMaxwell : public HIDDevice {

if (mapped == 0) {
// Toggle sidetone off
std::array<uint8_t, MSG_SIZE> toggle_cmd { 0x6, 0x9, 0x80, 0x5, 0x5a, 0x5, 0x0, 0x82, 0x2c, 0x7, 0x0, 0x1 };
std::array<uint8_t, MSG_SIZE> toggle_cmd { 0x06, 0x09, 0x80, 0x05, 0x5A, 0x05, 0x00, 0x82, 0x2C, 0x07, 0x00, 0x00 };
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was it before the other way around? As its now 0x00 as last byte

result = sendGetInputReport(device_handle, toggle_cmd);
if (!result) {
return result.error();
Expand All @@ -236,29 +261,11 @@ class AudezeMaxwell : public HIDDevice {

Result<InactiveTimeResult> setInactiveTime(hid_device* device_handle, uint8_t minutes) override
{
std::array<uint8_t, MSG_SIZE> cmd { 0x6, 0x10, 0x80, 0x5, 0x5a, 0xc, 0x0, 0x82, 0x2c, 0x1, 0x0 };
std::array<uint8_t, MSG_SIZE> cmd { 0x06, 0x10, 0x80, 0x05, 0x5A, 0x0C, 0x00, 0x82, 0x2C, 0x01, 0x00 };

if (minutes > 0) {
// Round to supported values
uint16_t final_minutes = 360;
if (minutes <= 5)
final_minutes = 5;
else if (minutes <= 10)
final_minutes = 10;
else if (minutes <= 15)
final_minutes = 15;
else if (minutes <= 30)
final_minutes = 30;
else if (minutes <= 45)
final_minutes = 45;
else if (minutes <= 60)
final_minutes = 60;
else if (minutes <= 90)
final_minutes = 90;
else if (minutes <= 120)
final_minutes = 120;
else if (minutes <= 240)
final_minutes = 240;
uint16_t final_minutes = map(minutes, 0, 90, 0, 360);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be semantically incorrect.

Passing in 5 minutes, the user would get 20


uint16_t seconds = final_minutes * 60;

Expand Down Expand Up @@ -349,5 +356,22 @@ class AudezeMaxwell : public HIDDevice {

return EqualizerPresetResult { .preset = preset, .total_presets = EQUALIZER_PRESETS_COUNT };
}

Result<NoiseFilterResult> setNoiseFilter(hid_device* device_handle, uint8_t level) override
{
// Maxwell 2 has three levels for the noise filter: high, low, and
// off (2,1 and 0)
if (level > 2) {
return DeviceError::invalidParameter("Noise filter level must be 0, 1, or 2");
}
std::array<uint8_t, MSG_SIZE> cmd { 0x06, 0x09, 0x80, 0x05, 0x5A, 0x05, 0x00, 0x00, 0x09, 0x24, 0x00, level };

auto result = sendGetInputReport(device_handle, cmd);
if (!result) {
return result.error();
}

return NoiseFilterResult { .level = level };
}
};
} // namespace headsetcontrol
Loading