2727import re
2828import usb .core
2929
30+ # First byte of the chatmix interrupt
31+ CHATMIX_OPCODE = 0x45
32+
33+ supported_devices = [
34+ {
35+ "name" : "Arctis 7P" ,
36+ "idVendor" : 0x1038 ,
37+ "idProduct" : 0x220e ,
38+ "hidIndex" : 7
39+ },
40+ {
41+ "name" : "Nova 7 WOW Edition" ,
42+ "idVendor" : 0x1038 ,
43+ "idProduct" : 0x227a ,
44+ "hidIndex" : 7
45+ },
46+ {
47+ "name" : "Arctis Nova 7 Gen 2" ,
48+ "idVendor" : 0x1038 ,
49+ "idProduct" : 0x227e ,
50+ "hidIndex" : 8
51+ }
52+ ]
3053
3154class Arctis7PlusChatMix :
3255 def __init__ (self ):
@@ -37,30 +60,12 @@ def __init__(self):
3760 self .log = self ._init_log ()
3861 self .log .info ("Initializing ac7pcm..." )
3962
40- # identify the arctis 7+ device
41- try :
42- # Support both Arctis 7P (0x220e) and Nova 7 WOW Edition (0x227a)
43- self .dev = usb .core .find (idVendor = 0x1038 , idProduct = 0x220e ) or \
44- usb .core .find (idVendor = 0x1038 , idProduct = 0x227a )
45- except Exception as e :
46- self .log .error ("""Failed to identify the Arctis 7+ device.
47- Please ensure it is connected.\n
48- Please note: This program only supports the '7+' or the Nova 7 WoW Edition models""" )
49- self .die_gracefully (trigger = "Couldn't find arctis7 model" )
50-
51- # select its interface and USB endpoint, and capture the endpoint address
52- try :
53- # interface index 7 of the Arctis 7+ is the USB HID for the ChatMix dial;
54- # its actual interface number on the device itself is 5.
55- self .interface = self .dev [0 ].interfaces ()[7 ]
56- self .interface_num = self .interface .bInterfaceNumber
57- self .endpoint = self .interface .endpoints ()[0 ]
58- self .addr = self .endpoint .bEndpointAddress
63+ # get the default sink id from pactl
64+ self .system_default_sink = os .popen ("pactl get-default-sink" ).read ().strip ()
65+ self .log .info (f"default sink identified as { self .system_default_sink } " )
5966
60- except Exception as e :
61- self .log .error ("""Failure to identify relevant
62- USB device's interface or endpoint. Shutting down...""" )
63- self .die_gracefully (exc = True , trigger = "identification of USB endpoint" )
67+ # Find a supported arctis device
68+ self ._bind_device ()
6469
6570 # detach if the device is active
6671 if self .dev .is_kernel_driver_active (self .interface_num ):
@@ -78,15 +83,45 @@ def _init_log(self):
7883 log .addHandler (stdout_handler )
7984 return (log )
8085
86+
87+ def _bind_device (self ):
88+ # Match on any devices which we have explicitly listed
89+ def is_supported (dev ):
90+ return any (device_info ["idVendor" ] == dev .idVendor and device_info ["idProduct" ] == dev .idProduct for device_info in supported_devices )
91+
92+ # TODO: multi-device support... for now we just bind to the first supported device we find
93+ matched_device = usb .core .find (find_all = False , custom_match = is_supported )
94+
95+ if not matched_device :
96+ all_device_names = ", " .join ([device_info ["name" ] for device_info in supported_devices ])
97+ self .log .error (f"""Failed to identify the Arctis device.
98+ Please ensure it is connected and supported.\n
99+ Supported models: { all_device_names } .""" )
100+ self .die_gracefully (trigger = "Couldn't find supported arctis model" )
101+ return
102+
103+ self .dev = matched_device
104+ device_info = next (d for d in supported_devices if d ["idProduct" ] == self .dev .idProduct )
105+ self .log .info (f"Found supported device: { device_info ['name' ]} ({ hex (device_info ['idVendor' ])} :{ hex (device_info ['idProduct' ])} )" )
106+
107+ # select its interface and USB endpoint, and capture the endpoint address
108+ try :
109+ self .interface = self .dev [0 ].interfaces ()[device_info ["hidIndex" ]]
110+ self .interface_num = self .interface .bInterfaceNumber
111+ self .endpoint = self .interface .endpoints ()[0 ]
112+ self .addr = self .endpoint .bEndpointAddress
113+
114+ except Exception as e :
115+ self .log .error ("""Failure to identify relevant
116+ USB device's interface or endpoint. Shutting down...""" )
117+ self .die_gracefully (exc = True , trigger = "identification of USB endpoint" )
118+
119+
81120 def _init_VAC (self ):
82121 """Get name of default sink, establish virtual sink
83122 and pipe its output to the default sink
84123 """
85124
86- # get the default sink id from pactl
87- self .system_default_sink = os .popen ("pactl get-default-sink" ).read ().strip ()
88- self .log .info (f"default sink identified as { self .system_default_sink } " )
89-
90125 # attempt to identify an Arctis sink via pactl
91126 try :
92127 pactl_short_sinks = os .popen ("pactl list short sinks" ).readlines ()
@@ -184,15 +219,18 @@ def start_modulator_signal(self):
184219 while True :
185220 try :
186221 # read the input of the USB signal. Signal is sent in 64-bit interrupt packets.
222+ # read_input[0] is the opcode. We only care about chatmix (0x45)
187223 # read_input[1] returns value to use for default device volume
188224 # read_input[2] returns the value to use for virtual device volume
189225 read_input = self .dev .read (self .addr , 64 )
190- default_device_volume = "{}%" .format (read_input [1 ])
191- virtual_device_volume = "{}%" .format (read_input [2 ])
192-
193- # os.system calls to issue the commands directly to pactl
194- os .system (f'pactl set-sink-volume Arctis_Game { default_device_volume } ' )
195- os .system (f'pactl set-sink-volume Arctis_Chat { virtual_device_volume } ' )
226+ if read_input [0 ] == CHATMIX_OPCODE :
227+ default_device_volume = "{}%" .format (read_input [1 ])
228+ virtual_device_volume = "{}%" .format (read_input [2 ])
229+
230+ # os.system calls to issue the commands directly to pactl
231+ os .system (f'pactl set-sink-volume Arctis_Game { default_device_volume } ' )
232+ os .system (f'pactl set-sink-volume Arctis_Chat { virtual_device_volume } ' )
233+ self .log .debug (f"Game Volume: { default_device_volume } | Chat Volume: { virtual_device_volume } " )
196234 except usb .core .USBTimeoutError :
197235 pass
198236 except usb .core .USBError :
@@ -223,7 +261,7 @@ def die_gracefully(self, sink_creation_fail=False, trigger=None):
223261 sys .exit (1 )
224262 else :
225263 self .log .info ("-" * 45 )
226- self .log .info ("Artcis 7+ ChatMix shut down gracefully... Bye Bye!" )
264+ self .log .info ("Arctis 7+ ChatMix shut down gracefully... Bye Bye!" )
227265 self .log .info ("-" * 45 )
228266 sys .exit (0 )
229267
0 commit comments