diff --git a/README.md b/README.md index 7114c30..c0cad1e 100644 --- a/README.md +++ b/README.md @@ -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 | | | | | | | | | | | | | | | | diff --git a/lib/devices/audeze_maxwell.hpp b/lib/devices/audeze_maxwell.hpp index 37d409b..2017ec3 100644 --- a/lib/devices/audeze_maxwell.hpp +++ b/lib/devices/audeze_maxwell.hpp @@ -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 { @@ -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, 13> UNIQUE_REQUESTS { { { 0x6, 0x8, 0x80, 0x5, 0x5a, 0x4, 0x0, 0x1, 0x9, 0x20 }, + // 15-packet initialization sequence + static constexpr std::array, 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 }, @@ -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, 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, 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 { @@ -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 @@ -129,37 +131,60 @@ 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 getDeviceStatus(hid_device* device_handle) const { - std::array, 5> status_buffs {}; + std::array, 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]; @@ -167,26 +192,26 @@ class AudezeMaxwell : public HIDDevice { 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; + + // 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; } @@ -208,7 +233,7 @@ class AudezeMaxwell : public HIDDevice { // Maxwell range: 0 to 31 uint8_t mapped = map(level, 0, 128, 0, 31); - std::array cmd { 0x6, 0x9, 0x80, 0x5, 0x5a, 0x5, 0x0, 0x0, 0x9, 0x2c, 0x0, mapped }; + std::array 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); @@ -218,7 +243,7 @@ class AudezeMaxwell : public HIDDevice { if (mapped == 0) { // Toggle sidetone off - std::array toggle_cmd { 0x6, 0x9, 0x80, 0x5, 0x5a, 0x5, 0x0, 0x82, 0x2c, 0x7, 0x0, 0x1 }; + std::array toggle_cmd { 0x06, 0x09, 0x80, 0x05, 0x5A, 0x05, 0x00, 0x82, 0x2C, 0x07, 0x00, 0x00 }; result = sendGetInputReport(device_handle, toggle_cmd); if (!result) { return result.error(); @@ -236,29 +261,11 @@ class AudezeMaxwell : public HIDDevice { Result setInactiveTime(hid_device* device_handle, uint8_t minutes) override { - std::array cmd { 0x6, 0x10, 0x80, 0x5, 0x5a, 0xc, 0x0, 0x82, 0x2c, 0x1, 0x0 }; + std::array 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); uint16_t seconds = final_minutes * 60; @@ -349,5 +356,22 @@ class AudezeMaxwell : public HIDDevice { return EqualizerPresetResult { .preset = preset, .total_presets = EQUALIZER_PRESETS_COUNT }; } + + Result 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 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