diff --git a/README.md b/README.md index 09185fe3..d33dddab 100644 --- a/README.md +++ b/README.md @@ -9,34 +9,39 @@ talking. This differs from a simple loopback via PulseAudio as you won't have an ## Supported Headsets -| Device | 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 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| Audeze Maxwell | x | x | | | x | x | x | | x | | | | | x | | | -| Corsair Headset Device | x | x | x | x | | | | | | | | | | | | | -| HyperX Cloud Alpha Wireless | x | x | | | x | | x | | | | | | | | | | -| HyperX Cloud Flight Wireless | | x | | | | | | | | | | | | | | | -| HyperX Cloud 3 | x | | | | | | | | | | | | | | | | -| Logitech G430 | x | | | | | | | | | | | | | | | | -| Logitech G432/G433 | x | | | | | | | | | | | | | | | | -| Logitech G533 | x | x | | | x | | | | | | | | | | | | -| Logitech G535 | x | x | | | x | | | | | | | | | | | | -| Logitech G930 | x | x | | | | | | | | | | | | | | | -| Logitech G633/G635/G733/G933/G935 | x | x | | x | | | | | | | | | | | | | -| Logitech G PRO Series | x | x | | | x | | | | | | | | | | | | -| Logitech G PRO X 2 | x | | | | x | | | | | | | | | | | | -| Logitech Zone Wired/Zone 750 | x | | | | | | x | x | | | | | | | | | -| SteelSeries Arctis (1/7X/7P) Wireless | x | x | | | x | | | | | | | | | | | | -| SteelSeries Arctis (7/Pro) | x | x | | x | x | x | | | | | | | | | | | -| SteelSeries Arctis 9 | x | x | | | x | x | | | | | | | | | | | -| SteelSeries Arctis Pro Wireless | x | x | | | x | | | | | | | | | | | | -| ROCCAT Elo 7.1 Air | | | | x | x | | | | | | | | | | | | -| ROCCAT Elo 7.1 USB | | | | x | | | | | | | | | | | | | -| SteelSeries Arctis Nova 3 | x | | | | | | | | x | x | | x | x | | | | -| SteelSeries Arctis Nova (5/5X) | x | x | | | x | x | | | x | x | x | x | x | x | | | -| SteelSeries Arctis Nova 7 | x | x | | | x | x | | | x | x | | x | x | x | x | x | -| SteelSeries Arctis 7+ | x | x | | | x | x | | | x | x | | | | | | | -| SteelSeries Arctis Nova Pro Wireless | x | x | | x | x | | | | x | x | | | | | | | -| HeadsetControl Test device | 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 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| Audeze Maxwell | All | x | x | | | x | x | x | | x | | | | | x | | | +| Corsair Headset Device | All | x | x | x | x | | | | | | | | | | | | | +| HyperX Cloud Alpha Wireless | All | x | x | | | x | | x | | | | | | | | | | +| HyperX Cloud Flight Wireless | All | | x | | | | | | | | | | | | | | | +| HyperX Cloud 3 | All | x | | | | | | | | | | | | | | | | +| Logitech G430 | All | x | | | | | | | | | | | | | | | | +| Logitech G432/G433 | All | x | | | | | | | | | | | | | | | | +| Logitech G533 | All | x | x | | | x | | | | | | | | | | | | +| Logitech G535 | All | x | x | | | x | | | | | | | | | | | | +| Logitech G930 | All | x | x | | | | | | | | | | | | | | | +| Logitech G633/G635/G733/G933/G935 | All | x | x | | x | | | | | | | | | | | | | +| Logitech G PRO Series | All | x | x | | | x | | | | | | | | | | | | +| Logitech G PRO X 2 | All | x | | | | x | | | | | | | | | | | | +| Logitech Zone Wired/Zone 750 | 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 | | | | | | | | | | | | +| ROCCAT Elo 7.1 Air | All | | | | x | x | | | | | | | | | | | | +| ROCCAT Elo 7.1 USB | All | | | | x | | | | | | | | | | | | | +| SteelSeries Arctis Nova 3 | All | x | | | | | | | | x | x | | x | x | | | | +| SteelSeries Arctis Nova 3P Wireless | L/M | x | x | | | 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 7+ | All | x | x | | | x | x | | | x | x | | | | | | | +| SteelSeries Arctis Nova Pro Wireless | All | x | x | | x | x | | | | x | x | | | | | | | +| HeadsetControl Test device | All | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | x | + +**Platform Legend:** +- **All**: Linux, macOS, and Windows +- **L/M**: Linux and macOS only (Windows not supported due to HID implementation differences) For non-supported headsets on Linux: There is a chance that you can set the sidetone via AlsaMixer diff --git a/src/device.c b/src/device.c index a566a24c..131d05e5 100644 --- a/src/device.c +++ b/src/device.c @@ -67,7 +67,8 @@ const char* const equalizer_filter_type_str[NUM_EQ_FILTER_TYPES] [EQ_FILTER_LOWPASS] = "lowpass", [EQ_FILTER_PEAKING] = "peaking", [EQ_FILTER_HIGHPASS] = "highpass", - [EQ_FILTER_HIGHSHELF] = "highshelf" + [EQ_FILTER_HIGHSHELF] = "highshelf", + [EQ_FILTER_NOTCH] = "notch" }; diff --git a/src/device.h b/src/device.h index 7c2c518f..40f297b0 100644 --- a/src/device.h +++ b/src/device.h @@ -16,6 +16,12 @@ /// Convert given number to bitmask #define B(X) (1 << X) +/// Platform support flags +#define PLATFORM_LINUX (1 << 0) +#define PLATFORM_MACOS (1 << 1) +#define PLATFORM_WINDOWS (1 << 2) +#define PLATFORM_ALL (PLATFORM_LINUX | PLATFORM_MACOS | PLATFORM_WINDOWS) + /// global read timeout in millisecounds extern int hsc_device_timeout; @@ -44,10 +50,8 @@ enum capabilities { NUM_CAPABILITIES }; -enum capabilitytype { - CAPABILITYTYPE_ACTION, - CAPABILITYTYPE_INFO -}; +enum capabilitytype { CAPABILITYTYPE_ACTION, + CAPABILITYTYPE_INFO }; /// Long name of every capability extern const char* const capabilities_str[NUM_CAPABILITIES]; @@ -59,7 +63,8 @@ extern const char* const capabilities_str_enum[NUM_CAPABILITIES]; struct capability_detail { // Usage page, only used when usageid is not 0; HID Protocol specific uint16_t usagepage; - // Used instead of interface when not 0, and only used on Windows currently; HID Protocol specific + // Used instead of interface when not 0, and only used on Windows currently; + // HID Protocol specific uint16_t usageid; /// Interface ID - zero means first enumerated interface! int interface; @@ -139,7 +144,8 @@ typedef struct { int value; /// Status depending on the feature (not used by all) int status2; - /// For error messages, "Charging", "Unavailable", etc. Should be free()d after use + /// For error messages, "Charging", "Unavailable", etc. Should be free()d + /// after use char* message; } FeatureResult; @@ -166,6 +172,7 @@ typedef enum { EQ_FILTER_PEAKING, EQ_FILTER_HIGHPASS, EQ_FILTER_HIGHSHELF, + EQ_FILTER_NOTCH, NUM_EQ_FILTER_TYPES } EqualizerFilterType; @@ -207,6 +214,9 @@ struct device { /// Name of device, used as information for the user char device_name[64]; + /// Bitmask of supported platforms (PLATFORM_LINUX | PLATFORM_MACOS | PLATFORM_WINDOWS) + uint8_t supported_platforms; + // Equalizer Infos EqualizerInfo* equalizer; EqualizerPresets* equalizer_presets; @@ -358,7 +368,8 @@ struct device { * specific to this hardware * -1 HIDAPI error */ - int (*send_equalizer)(hid_device* hid_device, struct equalizer_settings* settings); + int (*send_equalizer)(hid_device* hid_device, + struct equalizer_settings* settings); /** @brief Function pointer for setting headset parametric equalizer * @@ -372,11 +383,12 @@ struct device { * @returns > 0 on success * HSC_OUT_OF_BOUNDS on equalizer settings size out of range * specific to this hardware - * HSC_INVALID_ARG on equalizer filter type invalid/unsupported - * specific to this hardware - * -1 HIDAPI error + * HSC_INVALID_ARG on equalizer filter type + * invalid/unsupported specific to this hardware -1 HIDAPI + * error */ - int (*send_parametric_equalizer)(hid_device* hid_device, struct parametric_equalizer_settings* settings); + int (*send_parametric_equalizer)( + hid_device* hid_device, struct parametric_equalizer_settings* settings); /** @brief Function pointer for setting headset microphone mute LED brightness * @@ -391,7 +403,8 @@ struct device { * specific to this hardware * -1 HIDAPI error */ - int (*send_microphone_mute_led_brightness)(hid_device* hid_device, uint8_t num); + int (*send_microphone_mute_led_brightness)(hid_device* hid_device, + uint8_t num); /** @brief Function pointer for setting headset microphone volume * @@ -458,8 +471,8 @@ struct device { /** * @brief Node structure for a linked list of devices. * - * This structure represents a node in a linked list where each node contains a pointer to a device - * and a pointer to the next node in the list. + * This structure represents a node in a linked list where each node contains a + * pointer to a device and a pointer to the next node in the list. */ typedef struct DeviceListNode { struct device* element; diff --git a/src/device_registry.c b/src/device_registry.c index fd92f4f1..22c75bb6 100644 --- a/src/device_registry.c +++ b/src/device_registry.c @@ -22,6 +22,7 @@ #include "devices/steelseries_arctis_7_plus.h" #include "devices/steelseries_arctis_9.h" #include "devices/steelseries_arctis_nova_3.h" +#include "devices/steelseries_arctis_nova_3p_wireless.h" #include "devices/steelseries_arctis_nova_5.h" #include "devices/steelseries_arctis_nova_7.h" #include "devices/steelseries_arctis_nova_pro_wireless.h" @@ -68,6 +69,7 @@ void init_devices() add_device(elo71USB_init); // SteelSeries add_device(arctis_nova_3_init); + add_device(arctis_nova_3p_wireless_init); add_device(arctis_nova_5_init); add_device(arctis_nova_7_init); add_device(arctis_7_plus_init); @@ -78,6 +80,21 @@ void init_devices() void add_device(void (*init_func)(struct device**)) { + struct device* new_device = NULL; + + // Initialize the new device + init_func(&new_device); + + // Skip if device is NULL (platform not supported) + if (new_device == NULL) { + return; + } + + // Set default platform support to all platforms if not specified + if (new_device->supported_platforms == 0) { + new_device->supported_platforms = PLATFORM_ALL; + } + // Reallocate memory to accommodate the new device struct device** temp = realloc(devicelist, (num_devices + 1) * sizeof(struct device*)); if (temp == NULL) { @@ -86,8 +103,7 @@ void add_device(void (*init_func)(struct device**)) } devicelist = temp; - // Initialize the new device - init_func(&devicelist[num_devices]); + devicelist[num_devices] = new_device; num_devices++; } diff --git a/src/devices/CMakeLists.txt b/src/devices/CMakeLists.txt index c9ba00d4..4acca156 100644 --- a/src/devices/CMakeLists.txt +++ b/src/devices/CMakeLists.txt @@ -29,6 +29,8 @@ set(SOURCE_FILES ${SOURCE_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_9.h ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3.h ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3.c + ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3p_wireless.h + ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_3p_wireless.c ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_5.h ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_5.c ${CMAKE_CURRENT_SOURCE_DIR}/steelseries_arctis_nova_7.h diff --git a/src/devices/steelseries_arctis_nova_3p_wireless.c b/src/devices/steelseries_arctis_nova_3p_wireless.c new file mode 100644 index 00000000..72011f4e --- /dev/null +++ b/src/devices/steelseries_arctis_nova_3p_wireless.c @@ -0,0 +1,387 @@ +#include "../device.h" +#include "../utility.h" + +#include +#include +#include + +#define MSG_SIZE 64 + +#define ID_ARCTIS_NOVA_3P_WIRELESS 0x2269 + +#define HEADSET_ONLINE 0x03 +#define HEADSET_OFFLINE 0x02 + +#define BATTERY_MAX 0x64 +#define BATTERY_MIN 0x00 + +#define EQUALIZER_BANDS_COUNT 10 +#define EQUALIZER_GAIN_STEP 0.1 +#define EQUALIZER_GAIN_BASE 0 +#define EQUALIZER_GAIN_MIN -12 +#define EQUALIZER_GAIN_MAX 12 + +#define EQUALIZER_PRESETS_COUNT 4 + +#define EQUALIZER_Q_FACTOR_MIN 0.2 +#define EQUALIZER_Q_FACTOR_MAX 10.0 +#define EQUALIZER_Q_FACTOR_DEFAULT 1.414 +#define EQUALIZER_FREQ_MIN 20 +#define EQUALIZER_FREQ_MAX 20000 +#define EQUALIZER_FREQ_DISABLED 20001 +#define EQUALIZER_FILTERS (B(EQ_FILTER_LOWSHELF) | B(EQ_FILTER_LOWPASS) | B(EQ_FILTER_PEAKING) | B(EQ_FILTER_HIGHPASS) | B(EQ_FILTER_HIGHSHELF) | B(EQ_FILTER_NOTCH)) + +static EqualizerInfo equalizer = { EQUALIZER_BANDS_COUNT, 0, EQUALIZER_GAIN_STEP, EQUALIZER_GAIN_MIN, EQUALIZER_GAIN_MAX }; + +static float flat[EQUALIZER_BANDS_COUNT] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; +static float bass[EQUALIZER_BANDS_COUNT] = { 3.5f, 5.5f, 4, 1, -1.5f, -1.5f, -1, -1, -1, -1 }; +static float focus[EQUALIZER_BANDS_COUNT] = { -5, -3.5f, -1, -3.5f, -2.5f, 4, 6, -3.5f, 0 }; +static float smiley[EQUALIZER_BANDS_COUNT] = { 3, 3.5f, 1.5f, -1.5f, -4, -4, -2.5f, 1.5f, 3, 4 }; + +static EqualizerPresets equalizer_presets = { + EQUALIZER_PRESETS_COUNT, + { { "flat", flat }, + { "bass", bass }, + { "focus", focus }, + { "smiley", smiley } } +}; + +static ParametricEqualizerInfo parametric_equalizer = { + EQUALIZER_BANDS_COUNT, + EQUALIZER_GAIN_BASE, + EQUALIZER_GAIN_STEP, + EQUALIZER_GAIN_MIN, + EQUALIZER_GAIN_MAX, + EQUALIZER_Q_FACTOR_MIN, + EQUALIZER_Q_FACTOR_MAX, + EQUALIZER_FREQ_MIN, + EQUALIZER_FREQ_MAX, + EQUALIZER_FILTERS +}; + +static uint8_t EQUALIZER_FILTER_MAP[NUM_EQ_FILTER_TYPES] = { + [EQ_FILTER_PEAKING] = 1, + [EQ_FILTER_LOWPASS] = 2, + [EQ_FILTER_HIGHPASS] = 3, + [EQ_FILTER_LOWSHELF] = 4, + [EQ_FILTER_HIGHSHELF] = 5, + [EQ_FILTER_NOTCH] = 6, +}; + +static struct device device_arctis; + +static const uint16_t PRODUCT_IDS[] = { ID_ARCTIS_NOVA_3P_WIRELESS }; +static const uint8_t SAVE_DATA[MSG_SIZE] = { 0x09 }; + +static int arctis_nova_3p_wireless_send_sidetone(hid_device* device_handle, uint8_t num); +static int arctis_nova_3p_wireless_send_microphone_volume(hid_device* device_handle, uint8_t num); +static int arctis_nova_3p_wireless_send_inactive_time(hid_device* device_handle, uint8_t num); +static BatteryInfo arctis_nova_3p_wireless_request_battery(hid_device* device_handle); +static int arctis_nova_3p_send_equalizer(hid_device* device_handle, struct equalizer_settings* settings); +static int arctis_nova_3p_send_equalizer_preset(hid_device* device_handle, uint8_t num); +static int arctis_nova_3p_send_parametric_equalizer(hid_device* device_handle, struct parametric_equalizer_settings* settings); +static int arctis_nova_3p_write_device_band(struct parametric_equalizer_band* filter, uint8_t* data); +static int arctis_nova_3p_wireless_save_state(hid_device* device_handle); + +void arctis_nova_3p_wireless_init(struct device** device) +{ + device_arctis.idVendor = VENDOR_STEELSERIES; + device_arctis.idProductsSupported = PRODUCT_IDS; + device_arctis.numIdProducts = sizeof(PRODUCT_IDS) / sizeof(PRODUCT_IDS[0]); + device_arctis.equalizer = &equalizer; + device_arctis.equalizer_presets = &equalizer_presets; + device_arctis.parametric_equalizer = ¶metric_equalizer; + + strncpy(device_arctis.device_name, "SteelSeries Arctis Nova 3P Wireless", sizeof(device_arctis.device_name)); + + // Windows support not yet working - HID packet length issues + // Functions execute but have no effect on the device + // See: https://github.com/Sapd/HeadsetControl/pull/417 + device_arctis.supported_platforms = PLATFORM_LINUX | PLATFORM_MACOS; + + device_arctis.capabilities = B(CAP_SIDETONE) | B(CAP_INACTIVE_TIME) | B(CAP_MICROPHONE_VOLUME) | B(CAP_BATTERY_STATUS) | B(CAP_EQUALIZER) | B(CAP_EQUALIZER_PRESET) | B(CAP_PARAMETRIC_EQUALIZER); + + device_arctis.capability_details[CAP_SIDETONE] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_INACTIVE_TIME] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_MICROPHONE_VOLUME] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_BATTERY_STATUS] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_EQUALIZER] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_EQUALIZER_PRESET] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + device_arctis.capability_details[CAP_PARAMETRIC_EQUALIZER] = (struct capability_detail) { .usagepage = 0xffc0, .usageid = 0x1, .interface = 0 }; + + device_arctis.send_sidetone = &arctis_nova_3p_wireless_send_sidetone; + device_arctis.send_inactive_time = &arctis_nova_3p_wireless_send_inactive_time; + device_arctis.send_microphone_volume = &arctis_nova_3p_wireless_send_microphone_volume; + device_arctis.request_battery = &arctis_nova_3p_wireless_request_battery; + device_arctis.send_equalizer = &arctis_nova_3p_send_equalizer; + device_arctis.send_equalizer_preset = &arctis_nova_3p_send_equalizer_preset; + device_arctis.send_parametric_equalizer = &arctis_nova_3p_send_parametric_equalizer; + + *device = &device_arctis; +} + +static int arctis_nova_3p_wireless_send_sidetone(hid_device* device_handle, uint8_t num) +{ + num = map(num, 0, 128, 0, 0xa); + + uint8_t data[MSG_SIZE] = { 0x39, num }; + hid_send_feature_report(device_handle, data, MSG_SIZE); + + return arctis_nova_3p_wireless_save_state(device_handle); +} + +static int arctis_nova_3p_wireless_send_microphone_volume(hid_device* device_handle, uint8_t num) +{ + num = map(num, 0, 128, 0, 0x0e); + + uint8_t volume[MSG_SIZE] = { 0x37, num }; + hid_send_feature_report(device_handle, volume, MSG_SIZE); + + return arctis_nova_3p_wireless_save_state(device_handle); +} + +static int arctis_nova_3p_wireless_send_inactive_time(hid_device* device_handle, uint8_t num) +{ + // the unit of time is the minute + if (num >= 90) { + num = 90; + } else if (num >= 75) { + num = 75; + } else if (num >= 60) { + num = 60; + } else if (num >= 45) { + num = 45; + } else if (num >= 30) { + num = 30; + } else if (num >= 15) { + num = 15; + } else if (num >= 10) { + num = 10; + } else if (num >= 5) { + num = 5; + } else if (num >= 1) { + num = 1; + } else { + // this will NEVER turn off the headset after a period of inactivity + num = 0; + } + + uint8_t data[MSG_SIZE] = { 0xa3, num }; + hid_send_feature_report(device_handle, data, MSG_SIZE); + + return arctis_nova_3p_wireless_save_state(device_handle); +} + +static BatteryInfo arctis_nova_3p_wireless_request_battery(hid_device* device_handle) +{ + int r = 0; + + BatteryInfo info = { .status = BATTERY_UNAVAILABLE, .level = -1 }; + + uint8_t data[MSG_SIZE] = { 0xb0 }; + r = hid_send_feature_report(device_handle, data, MSG_SIZE); + + if (r < 0) { + info.status = BATTERY_HIDERROR; + return info; + } + + if (r == 0) { + info.status = BATTERY_TIMEOUT; + return info; + } + + r = hid_read_timeout(device_handle, data, 4, hsc_device_timeout); + + if (r < 0) { + info.status = BATTERY_HIDERROR; + return info; + } + + if (r == 0) { + info.status = BATTERY_TIMEOUT; + return info; + } + + if (data[1] == HEADSET_OFFLINE) { + info.status = BATTERY_UNAVAILABLE; + return info; + } + + info.status = BATTERY_AVAILABLE; + int bat = data[3]; + info.level = map(bat, BATTERY_MIN, BATTERY_MAX, 0, 100); + return info; +} + +static int arctis_nova_3p_send_equalizer(hid_device* device_handle, struct equalizer_settings* settings) +{ + if (settings->size != EQUALIZER_BANDS_COUNT) { + printf("Device only supports %d bands.\n", EQUALIZER_BANDS_COUNT); + return HSC_OUT_OF_BOUNDS; + } + uint8_t data[MSG_SIZE] = { 0x33 }; + + // default frequencies for each band (2 bytes per band) + const unsigned char band_freq_le[2 * EQUALIZER_BANDS_COUNT] = { + 0x20, + 0x0, + 0x40, + 0x0, + 0x7d, + 0x0, + 0xfa, + 0x0, + 0xf4, + 0x1, + 0xe8, + 0x3, + 0xd0, + 0x7, + 0xa0, + 0xf, + 0x40, + 0x1f, + 0x80, + 0x3e, + }; + + for (size_t i = 0; i < settings->size; i++) { + float gain_value = settings->bands_values[i]; + if (gain_value < EQUALIZER_GAIN_MIN || gain_value > EQUALIZER_GAIN_MAX) { + printf("Device only supports gains ranging from %d to %d.\n", EQUALIZER_GAIN_MIN, EQUALIZER_GAIN_MAX); + return HSC_OUT_OF_BOUNDS; + } + uint8_t raw_gain_value; + if (gain_value < 0) { + gain_value = (-gain_value) / EQUALIZER_GAIN_STEP; + raw_gain_value = (0xff - ((uint8_t)gain_value)) + 1; + } else { + raw_gain_value = (uint8_t)(gain_value / EQUALIZER_GAIN_STEP); + } + + data[1 + 6 * i + 0] = band_freq_le[2 * i]; + data[1 + 6 * i + 1] = band_freq_le[2 * i + 1]; + data[1 + 6 * i + 2] = 0x1; // use Peaking EQ by default + data[1 + 6 * i + 3] = raw_gain_value; + // use a default quality factor of 1.414 (approximated sqrt(2)) multiplied by 1000 to have a fixed 16 bit number number + data[1 + 6 * i + 4] = 0x86; + data[1 + 6 * i + 5] = 0x5; + } + + hid_send_feature_report(device_handle, data, MSG_SIZE); + return arctis_nova_3p_wireless_save_state(device_handle); +} + +static int arctis_nova_3p_send_equalizer_preset(hid_device* device_handle, uint8_t num) +{ + struct equalizer_settings preset; + preset.size = EQUALIZER_BANDS_COUNT; + switch (num) { + case 0: { + preset.bands_values = &flat[0]; + break; + } + case 1: { + preset.bands_values = &bass[0]; + break; + } + case 2: { + preset.bands_values = &focus[0]; + break; + } + case 3: { + preset.bands_values = &smiley[0]; + break; + } + default: + printf("Preset %d out of bounds\n", num); + return HSC_OUT_OF_BOUNDS; + } + return arctis_nova_3p_send_equalizer(device_handle, &preset); +} + +static int arctis_nova_3p_send_parametric_equalizer(hid_device* device_handle, struct parametric_equalizer_settings* settings) +{ + struct parametric_equalizer_band disabled_band = { + .frequency = EQUALIZER_FREQ_DISABLED, + .gain = 0, + .q_factor = EQUALIZER_Q_FACTOR_DEFAULT, + .type = EQ_FILTER_PEAKING, + }; + + if (settings->size > EQUALIZER_BANDS_COUNT) { + printf("Device only supports up to %d equalizer bands.\n", EQUALIZER_BANDS_COUNT); + return HSC_OUT_OF_BOUNDS; + } + + uint8_t data[MSG_SIZE] = { 0x33 }; + int error = 0; + + for (size_t i = 0; i < EQUALIZER_BANDS_COUNT; i++) { + if (i < settings->size) { + error = arctis_nova_3p_write_device_band(&settings->bands[i], &data[1 + i * 6]); + } else { + error = arctis_nova_3p_write_device_band(&disabled_band, &data[1 + i * 6]); + } + if (error != 0) { + return error; + } + } + + hid_send_feature_report(device_handle, data, MSG_SIZE); + return arctis_nova_3p_wireless_save_state(device_handle); +} + +static int arctis_nova_3p_write_device_band(struct parametric_equalizer_band* filter, uint8_t* data) +{ + // fprintf(stderr, "freq: %g; gain: %g; q: %g; type: %d\n", filter->frequency, filter->gain, filter->q_factor, filter->type); + if (filter->frequency != EQUALIZER_FREQ_DISABLED) { + if (filter->frequency < EQUALIZER_FREQ_MIN || filter->frequency > EQUALIZER_FREQ_MAX) { + printf("Device only supports filter frequencies ranging from %d to %d.\n", EQUALIZER_FREQ_MIN, EQUALIZER_FREQ_MAX); + return HSC_OUT_OF_BOUNDS; + } + } + if (!has_capability(EQUALIZER_FILTERS, (int)filter->type)) { + printf("Unsupported filter type.\n"); + return HSC_ERROR; + } + if (filter->q_factor < EQUALIZER_Q_FACTOR_MIN || filter->q_factor > EQUALIZER_Q_FACTOR_MAX) { + printf("Device only supports filter q-factor ranging from %g to %g.\n", EQUALIZER_Q_FACTOR_MIN, EQUALIZER_Q_FACTOR_MAX); + return HSC_OUT_OF_BOUNDS; + } + if (filter->gain < EQUALIZER_GAIN_MIN || filter->gain > EQUALIZER_GAIN_MAX) { + printf("Device only supports filter gains ranging from %d to %d.\n", EQUALIZER_GAIN_MIN, EQUALIZER_GAIN_MAX); + return HSC_OUT_OF_BOUNDS; + } + + // write filter frequency + data[0] = (uint16_t)filter->frequency & 0xFF; // low byte + data[1] = ((uint16_t)filter->frequency >> 8) & 0xFF; // high byte + + // write filter type + data[2] = EQUALIZER_FILTER_MAP[filter->type]; + + // write filter gain + float gain_value = filter->gain; + uint8_t raw_gain_value; + if (gain_value < 0) { + gain_value = (-gain_value) / EQUALIZER_GAIN_STEP; + raw_gain_value = (0xff - ((uint8_t)gain_value)) + 1; + } else { + raw_gain_value = (uint8_t)(gain_value / EQUALIZER_GAIN_STEP); + } + data[3] = raw_gain_value; + + // write filter q-factor + uint16_t q = filter->q_factor * 1000; + data[4] = q & 0xFF; // low byte + data[5] = (q >> 8) & 0xFF; // high byte + + return 0; +} + +static int arctis_nova_3p_wireless_save_state(hid_device* device_handle) +{ + return hid_send_feature_report(device_handle, SAVE_DATA, MSG_SIZE); +} diff --git a/src/devices/steelseries_arctis_nova_3p_wireless.h b/src/devices/steelseries_arctis_nova_3p_wireless.h new file mode 100644 index 00000000..99bf07d8 --- /dev/null +++ b/src/devices/steelseries_arctis_nova_3p_wireless.h @@ -0,0 +1,3 @@ +#pragma once + +void arctis_nova_3p_wireless_init(struct device** device); diff --git a/src/main.c b/src/main.c index 92405963..a50eadd5 100644 --- a/src/main.c +++ b/src/main.c @@ -155,13 +155,13 @@ static void print_readmetable() int i = 0; struct device* device_found; - printf("| Device |"); + printf("| Device | Platform |"); for (int j = 0; j < NUM_CAPABILITIES; j++) { printf(" %s |", capabilities_str[j]); } printf("\n"); - printf("| --- |"); + printf("| --- | --- |"); for (int j = 0; j < NUM_CAPABILITIES; j++) { printf(" --- |"); } @@ -170,6 +170,26 @@ static void print_readmetable() while (iterate_devices(i++, &device_found) == 0) { printf("| %s |", device_found->device_name); + // Display platform support based on supported_platforms flags + uint8_t platforms = device_found->supported_platforms; + if (platforms == PLATFORM_ALL) { + printf(" All |"); + } else if (platforms == (PLATFORM_LINUX | PLATFORM_MACOS)) { + printf(" L/M |"); + } else if (platforms == (PLATFORM_LINUX | PLATFORM_WINDOWS)) { + printf(" L/W |"); + } else if (platforms == (PLATFORM_MACOS | PLATFORM_WINDOWS)) { + printf(" M/W |"); + } else if (platforms == PLATFORM_LINUX) { + printf(" L |"); + } else if (platforms == PLATFORM_MACOS) { + printf(" M |"); + } else if (platforms == PLATFORM_WINDOWS) { + printf(" W |"); + } else { + printf(" ? |"); // Unknown/invalid combination + } + for (int j = 0; j < NUM_CAPABILITIES; j++) { if (device_has_capability(device_found, j)) { printf(" x |"); @@ -271,7 +291,7 @@ static FeatureResult handle_feature(struct device* device_found, hid_device** de switch (cap) { case CAP_SIDETONE: - ret = device_found->send_sidetone(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_sidetone(*device_handle, (uint8_t)*(int*)param); break; case CAP_BATTERY_STATUS: { @@ -307,15 +327,15 @@ static FeatureResult handle_feature(struct device* device_found, hid_device** de } case CAP_NOTIFICATION_SOUND: - ret = device_found->notification_sound(*device_handle, (uint8_t) * (int*)param); + ret = device_found->notification_sound(*device_handle, (uint8_t)*(int*)param); break; case CAP_LIGHTS: - ret = device_found->switch_lights(*device_handle, (uint8_t) * (int*)param); + ret = device_found->switch_lights(*device_handle, (uint8_t)*(int*)param); break; case CAP_INACTIVE_TIME: - ret = device_found->send_inactive_time(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_inactive_time(*device_handle, (uint8_t)*(int*)param); break; case CAP_CHATMIX_STATUS: @@ -334,15 +354,15 @@ static FeatureResult handle_feature(struct device* device_found, hid_device** de return result; case CAP_VOICE_PROMPTS: - ret = device_found->switch_voice_prompts(*device_handle, (uint8_t) * (int*)param); + ret = device_found->switch_voice_prompts(*device_handle, (uint8_t)*(int*)param); break; case CAP_ROTATE_TO_MUTE: - ret = device_found->switch_rotate_to_mute(*device_handle, (uint8_t) * (int*)param); + ret = device_found->switch_rotate_to_mute(*device_handle, (uint8_t)*(int*)param); break; case CAP_EQUALIZER_PRESET: - ret = device_found->send_equalizer_preset(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_equalizer_preset(*device_handle, (uint8_t)*(int*)param); break; case CAP_EQUALIZER: @@ -354,23 +374,23 @@ static FeatureResult handle_feature(struct device* device_found, hid_device** de break; case CAP_MICROPHONE_MUTE_LED_BRIGHTNESS: - ret = device_found->send_microphone_mute_led_brightness(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_microphone_mute_led_brightness(*device_handle, (uint8_t)*(int*)param); break; case CAP_MICROPHONE_VOLUME: - ret = device_found->send_microphone_volume(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_microphone_volume(*device_handle, (uint8_t)*(int*)param); break; case CAP_VOLUME_LIMITER: - ret = device_found->send_volume_limiter(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_volume_limiter(*device_handle, (uint8_t)*(int*)param); break; case CAP_BT_WHEN_POWERED_ON: - ret = device_found->send_bluetooth_when_powered_on(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_bluetooth_when_powered_on(*device_handle, (uint8_t)*(int*)param); break; case CAP_BT_CALL_VOLUME: - ret = device_found->send_bluetooth_call_volume(*device_handle, (uint8_t) * (int*)param); + ret = device_found->send_bluetooth_call_volume(*device_handle, (uint8_t)*(int*)param); break; case NUM_CAPABILITIES: