Switch from pyusb to hidapi, update readme
This commit is contained in:
parent
3747fef22e
commit
57fdea9442
4 changed files with 190 additions and 190 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
__pycache__/
|
||||
env/
|
133
README.md
133
README.md
|
@ -1,14 +1,5 @@
|
|||
# Arctis Nova Pro Wireless ChatMix on Linux
|
||||
|
||||
### In this document
|
||||
|
||||
1. [About this project](#about-this-project)
|
||||
2. [Disclaimer](#disclaimer)
|
||||
3. [My Findings](#my-findings)
|
||||
4. [Usage](#usage)
|
||||
5. [What's Next](#whats-next)
|
||||
6. [License](#license)
|
||||
|
||||
## About this project
|
||||
|
||||
Some SteelSeries headsets have a feature called ChatMix where you can easily adjust game and chat audio volume on the headphones or dongle.
|
||||
|
@ -17,11 +8,11 @@ In previous SteelSeries headsets ChatMix was always a hardware feature. It worke
|
|||
|
||||
In newer generations of their headsets however, in particular the Arctis Nova Pro Wireless, this feature was taken out of the hardware itself, and made into a feature of their audio software called Sonar.
|
||||
|
||||
Sonar of course only works on Windows (and requires a SteelSeries account!), so everyone using other platforms were shit out of luck, even though these headphones are far from cheap.
|
||||
Sonar of course only works on Windows and requires a SteelSeries account.
|
||||
|
||||
Even though it is now a software feature, the hardware can still control it, but only when Sonar activated this feature on the base station. You can toggle between normal volume controls and ChatMix by pressing down on the volume dial on either the headset or base station.
|
||||
|
||||
I used ChatMix a lot on my previous SteelSeries headset, and I want to be able to use it on this one, so I started looking into how I can control 2 virtual PipeWire sinks with the ChatMix controls. This project is the result.
|
||||
I wanted to be able to use ChatMix on linux, so I reverse engineered the communication between Sonar and the base station to control 2 virtual PipeWire sinks.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
@ -29,76 +20,44 @@ THIS PROJECT HAS NO ASSOCIATION TO STEELSERIES, NOR IS IT IN ANY WAY SUPPORTED B
|
|||
|
||||
I AM NOT RESPONSIBLE FOR BRICKED/BROKEN DEVICES NOR DO I GUARANTEE IT WILL WORK FOR YOU.
|
||||
|
||||
USING ANYTHING IN THIS PROJECT *MIGHT* VOID YOUR WARRANTY AND IS AT YOUR OWN RISK
|
||||
|
||||
I started this project knowing nothing about USB (I still don't), so I might be overcomplicating everything. The scripts I created were quickly thrown together and don't really integrate with anything. I might try to improve this later, but I also might not, as it does work for me in it's current state.
|
||||
|
||||
## My findings
|
||||
|
||||
I started by installing SteelSeries GG and Sonar in a Windows 11 VM and passing through the base station USB device. On the (Linux) host I used Wireshark to see what happened when I enabled Sonar.
|
||||
|
||||
This device uses USB HID events to both configure and receive data from the base station. I identified which ones enabled what features and tried activating these on Linux using the /dev/hidraw* device. See `commands.sh` for more information.
|
||||
|
||||
I am on MCU firmware version `01.29.27` and DSP firmware version `00.03.82`.
|
||||
|
||||
### Protocol description
|
||||
|
||||
Again, I don't really know anything about USB, so I could be wrong about a lot of this, but this is the protocol as I understand it
|
||||
|
||||
See `nova.py` for a commented example implementation.
|
||||
|
||||
The controls and data output are on USB Interface 4 (`bInterfaceNumber=4`). This interface has 2 endpoint, 1 for sending data (`0x04`) and 1 for receiving data (`0x84`).
|
||||
|
||||
The HID Data messages are structured as follows:
|
||||
|
||||
- The first byte decides wether we are sending or receiving. `0x6` means we sent it, `0x7` means we received it from the base station
|
||||
- The second byte specifies the parameter, eg. `73` (`0x49`) to enable ChatMix
|
||||
- The next bytes contain the value for that parameter, some parameters use 1 byte, some use more.
|
||||
- Because the message should be 64 bytes long, the unused bytes should all be `0`
|
||||
|
||||
These are the parameters I have found:
|
||||
<br>
|
||||
*(There are quite a few more parameters, like sidetone and other settings, I just haven't documented those yet.)*
|
||||
|
||||
| Option | Description | Parameters | Range | Notes |
|
||||
| ------ | ----------- | ---------- | ----- | ----- |
|
||||
| 73 | ChatMix State | - Boolean: State | 0-1 |
|
||||
| 141 | Sonar Icon State | - Boolean: State | 0-1 | I think this only toggles the icon, but that could be wrong
|
||||
| 37 | Volume Attenuation | - Integer: Attenuation | 0-56 | 0=max<br>56=mute |
|
||||
| 69 | ChatMix Controls | - Integer: Game Volume<br>- Integer: Chat Volume | 0-100 |
|
||||
| 46 | EQ Preset | - Integer: Preset | 0-18 | Preset 4 is the custom EQ profile
|
||||
| 49 | Custom EQ Controls | - Integer: EQ Bar<br>- Integer: Value | 0-40 | On the base station the value ranges from -20 to 20
|
||||
USING ANYTHING IN THIS PROJECT _MIGHT_ VOID YOUR WARRANTY AND IS AT YOUR OWN RISK.
|
||||
|
||||
## Usage
|
||||
|
||||
For this project I created a simple Python program to both enable the controls and use them to control 2 virtual sound devices.
|
||||
|
||||
### Dependencies
|
||||
- Python 3 (I used 3.12, it may or may not work on older versions)
|
||||
- PyUSB
|
||||
|
||||
- Python 3
|
||||
- python-hidapi
|
||||
- PipeWire
|
||||
- pactl
|
||||
|
||||
On Fedora (assuming PipeWire is already setup) these can be installed with:
|
||||
On Fedora these can be installed with:
|
||||
|
||||
```
|
||||
sudo dnf install pulseaudio-utils python3 python3-pyusb
|
||||
sudo dnf install pulseaudio-utils python3 python3-hidapi
|
||||
```
|
||||
|
||||
On Debian based systems (like Ubuntu) these can be installed with:
|
||||
On Debian based systems (like Ubuntu or Pop!_OS) these can be installed with:
|
||||
|
||||
```
|
||||
sudo apt install pulseaudio-utils python3 python3-usb
|
||||
sudo apt install pulseaudio-utils python3 python3-hid
|
||||
```
|
||||
|
||||
### Install
|
||||
|
||||
Clone this repo and cd into it
|
||||
|
||||
```
|
||||
git clone https://git.dymstro.nl/Dymstro/nova-chatmix-linux.git
|
||||
cd nova-chatmix-linux
|
||||
```
|
||||
|
||||
To be able to run the script as a non-root user, some udev rules need to be applied. This will allow regular users to access the base station USB device. It also starts the script when it gets plugged in (only when the systemd service is set up).
|
||||
To be able to run the script as a non-root user, some udev rules need to be applied. This will allow regular users to access the base station USB device. It also starts the script when it gets plugged in (only when the systemd service is also set up).
|
||||
|
||||
Copy `50-nova-pro-wireless.rules` to `/etc/udev/rules.d` and reload udev rules:
|
||||
|
||||
```
|
||||
sudo cp 50-nova-pro-wireless.rules /etc/udev/rules.d/
|
||||
|
||||
|
@ -107,6 +66,7 @@ sudo udevadm trigger
|
|||
```
|
||||
|
||||
If you want to run this script on startup you can add and enable the systemd service
|
||||
|
||||
```
|
||||
## The systemd service expects the script in .local/bin
|
||||
# Create the folder if it doesn't exist
|
||||
|
@ -126,8 +86,9 @@ systemctl --user enable nova-chatmix --now
|
|||
|
||||
### Run
|
||||
|
||||
You can now run the python script to use ChatMix.
|
||||
You can now run the python script to use ChatMix.
|
||||
This will create 2 virtual sound devices:
|
||||
|
||||
- NovaGame for game/general audio
|
||||
- NovaChat for chat audio
|
||||
|
||||
|
@ -140,37 +101,41 @@ This command does not generate any output, but the Sonar icon should now be visi
|
|||
|
||||
To use ChatMix select NovaGame as your main audio output, and select NovaChat as the output in your voice chat software of choice.
|
||||
|
||||
|
||||
ChatMix should now work. You can toggle between volume and ChatMix by pressing the dial.
|
||||
|
||||
### Use as a library
|
||||
## Details
|
||||
|
||||
You should also be able to use nova.py as a library.
|
||||
I started by installing SteelSeries GG and Sonar in a Windows 11 VM and passing through the base station USB device. On the Linux host I used Wireshark to see what happened when I enabled Sonar.
|
||||
|
||||
This device uses USB HID events to both configure and receive data from the base station. I identified which ones enabled what features and tried activating these on Linux using the /dev/hidraw\* device. See `commands.sh` for more information.
|
||||
|
||||
I am on MCU firmware version `01.29.27` and DSP firmware version `00.03.82`.
|
||||
|
||||
### Protocol description
|
||||
|
||||
See `nova.py` for a commented example implementation.
|
||||
|
||||
The controls and data output are on USB Interface 4 (`bInterfaceNumber=4`). This interface has 2 endpoint, 1 for sending data (`0x04`) and 1 for receiving data (`0x84`).
|
||||
|
||||
The HID Data messages are structured as follows:
|
||||
|
||||
- The first byte decides wether we are sending or receiving. `0x6` means we sent it, `0x7` means we received it from the base station
|
||||
- The second byte specifies the parameter, eg. `73` (`0x49`) to enable ChatMix
|
||||
- The next bytes contain the value for that parameter, some parameters use 1 byte, some use more.
|
||||
- Because the message should be 64 bytes long, the unused bytes should all be `0`
|
||||
|
||||
These are the parameters I have found:
|
||||
<br>
|
||||
Example:
|
||||
_(There are quite a few more parameters, like sidetone and other settings, I just haven't documented those yet.)_
|
||||
|
||||
```
|
||||
from nova import NovaProWireless
|
||||
|
||||
nova = NovaProWireless()
|
||||
nova.enable_sonar_icon()
|
||||
nova.enable_chatmix()
|
||||
|
||||
# Output all received messages
|
||||
nova.print_output(True)
|
||||
```
|
||||
|
||||
## What's Next
|
||||
|
||||
Currently none of this is very polished, it should work, but that's about it. ~~I would like to make this a bit more integrated, so that it just works without having to run the python script every time.~~ I added a systemd service to automate starting the script.
|
||||
|
||||
There are also some other projects that try to make headset features work on linux, for example [HeadsetControl](https://github.com/Sapd/HeadsetControl). I might want to try and get some of this implemented in there.
|
||||
|
||||
I also want to figure out if this could be something that happens by default in PipeWire (or somewhere else). So that users of the headset don't need anything beside what already comes with their distribution.
|
||||
|
||||
While I'd like to do these things, I don't yet know if I will. Even in the current state of the project, it does work for me, so while I'd like for this to be a temporary solution, it might just stay like this.
|
||||
|
||||
If anyone else wants to work on these things, please do, I'd love to see it.
|
||||
| Option | Description | Parameters | Range | Notes |
|
||||
| ------ | ------------------ | ------------------------------------------------ | ----- | ----------------------------------------------------------- |
|
||||
| 73 | ChatMix State | - Boolean: State | 0-1 |
|
||||
| 141 | Sonar Icon State | - Boolean: State | 0-1 | I think this only toggles the icon, but that could be wrong |
|
||||
| 37 | Volume Attenuation | - Integer: Attenuation | 0-56 | 0=max<br>56=mute |
|
||||
| 69 | ChatMix Controls | - Integer: Game Volume<br>- Integer: Chat Volume | 0-100 |
|
||||
| 46 | EQ Preset | - Integer: Preset | 0-18 | Preset 4 is the custom EQ profile |
|
||||
| 49 | Custom EQ Controls | - Integer: EQ Bar<br>- Integer: Value | 0-40 | On the base station the value ranges from -20 to 20 |
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
[Unit]
|
||||
Description=This will enable ChatMix for the Steelseries Arctis Nova Pro Wireless
|
||||
Description=Enable ChatMix for the Steelseries Arctis Nova Pro Wireless
|
||||
After=pipewire.service pipewire-pulse.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Restart=no
|
||||
Type=simple
|
||||
ExecStartPre=/bin/sleep 1
|
||||
ExecStart=/usr/bin/python3 %h/.local/bin/nova.py
|
||||
ExecStart=%h/.local/bin/nova.py
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
|
239
nova.py
239
nova.py
|
@ -2,9 +2,52 @@
|
|||
|
||||
# Licensed under the 0BSD
|
||||
|
||||
from signal import SIGINT, SIGTERM, signal
|
||||
from subprocess import Popen, check_output
|
||||
from signal import signal, SIGINT, SIGTERM
|
||||
from usb.core import find, USBTimeoutError, USBError
|
||||
|
||||
from hid import device
|
||||
from hid import enumerate as hidenumerate
|
||||
|
||||
CMD_PACTL = "pactl"
|
||||
CMD_PWLOOPBACK = "pw-loopback"
|
||||
|
||||
|
||||
class ChatMix:
|
||||
# Create virtual pipewire sinks
|
||||
def __init__(self, output_sink: str, main_sink: str, chat_sink: str):
|
||||
self.main_sink = main_sink
|
||||
self.chat_sink = chat_sink
|
||||
self.main_sink_process = self._create_virtual_sink(main_sink, output_sink)
|
||||
self.chat_sink_process = self._create_virtual_sink(chat_sink, output_sink)
|
||||
|
||||
def set_main_volume(self, volume: int):
|
||||
self._set_volume(self.main_sink, volume)
|
||||
|
||||
def set_chat_volume(self, volume: int):
|
||||
self._set_volume(self.chat_sink, volume)
|
||||
|
||||
def set_volumes(self, main_volume: int, chat_volume: int):
|
||||
self.set_main_volume(main_volume)
|
||||
self.set_chat_volume(chat_volume)
|
||||
|
||||
def close(self):
|
||||
self.main_sink_process.terminate()
|
||||
self.chat_sink_process.terminate()
|
||||
|
||||
def _create_virtual_sink(self, name: str, output_sink: str) -> Popen:
|
||||
return Popen(
|
||||
[
|
||||
CMD_PWLOOPBACK,
|
||||
"-P",
|
||||
output_sink,
|
||||
"--capture-props=media.class=Audio/Sink",
|
||||
"-n",
|
||||
name,
|
||||
]
|
||||
)
|
||||
|
||||
def _set_volume(self, sink: str, volume: int):
|
||||
Popen([CMD_PACTL, "set-sink-volume", f"input.{sink}", f"{volume}%"])
|
||||
|
||||
|
||||
class NovaProWireless:
|
||||
|
@ -15,11 +58,11 @@ class NovaProWireless:
|
|||
# bInterfaceNumber
|
||||
INTERFACE = 0x4
|
||||
|
||||
# bEndpointAddress
|
||||
ENDPOINT_TX = 0x4 # EP 4 OUT
|
||||
ENDPOINT_RX = 0x84 # EP 4 IN
|
||||
# HID Message length
|
||||
MSGLEN = 63
|
||||
|
||||
MSGLEN = 64 # Total USB packet is 128 bytes, data is last 64 bytes.
|
||||
# Message read timeout
|
||||
READ_TIMEOUT = 1000
|
||||
|
||||
# First byte controls data direction.
|
||||
TX = 0x6 # To base station.
|
||||
|
@ -28,29 +71,25 @@ class NovaProWireless:
|
|||
# Second Byte
|
||||
# This is a very limited list of options, you can control way more. I just haven't implemented those options (yet)
|
||||
## As far as I know, this only controls the icon.
|
||||
OPT_SONAR_ICON = 141
|
||||
## Enabling this options enables the ability to switch between volume and ChatMix.
|
||||
OPT_CHATMIX_ENABLE = 73
|
||||
OPT_SONAR_ICON = 0x8D
|
||||
## Enabling this option enables the ability to switch between volume and ChatMix.
|
||||
OPT_CHATMIX_ENABLE = 0x49
|
||||
## Volume controls, 1 byte
|
||||
OPT_VOLUME = 37
|
||||
OPT_VOLUME = 0x25
|
||||
## ChatMix controls, 2 bytes show and control game and chat volume.
|
||||
OPT_CHATMIX = 69
|
||||
OPT_CHATMIX = 0x45
|
||||
## EQ controls, 2 bytes show and control which band and what value.
|
||||
OPT_EQ = 49
|
||||
OPT_EQ = 0x31
|
||||
## EQ preset controls, 1 byte sets and shows enabled preset. Preset 4 is the custom preset required for OPT_EQ.
|
||||
OPT_EQ_PRESET = 46
|
||||
OPT_EQ_PRESET = 0x2E
|
||||
|
||||
# PipeWire Names
|
||||
## This is automatically detected, can be set manually by overriding this variable
|
||||
PW_ORIGINAL_SINK = None
|
||||
## String used to automatically select output sink
|
||||
PW_OUTPUT_SINK_AUTODETECT = "SteelSeries_Arctis_Nova_Pro_Wireless"
|
||||
## Names of virtual sound devices
|
||||
PW_GAME_SINK = "NovaGame"
|
||||
PW_CHAT_SINK = "NovaChat"
|
||||
|
||||
# PipeWire virtual sink processes
|
||||
PW_LOOPBACK_GAME_PROCESS = None
|
||||
PW_LOOPBACK_CHAT_PROCESS = None
|
||||
|
||||
# Keeps track of enabled features for when close() is called
|
||||
CHATMIX_CONTROLS_ENABLED = False
|
||||
SONAR_ICON_ENABLED = False
|
||||
|
@ -58,129 +97,105 @@ class NovaProWireless:
|
|||
# Stops processes when program exits
|
||||
CLOSE = False
|
||||
|
||||
# Selects correct device, and makes sure we can control it
|
||||
def __init__(self):
|
||||
self.dev = find(idVendor=self.VID, idProduct=self.PID)
|
||||
if self.dev is None:
|
||||
raise ValueError("Device not found")
|
||||
if self.dev.is_kernel_driver_active(self.INTERFACE):
|
||||
self.dev.detach_kernel_driver(self.INTERFACE)
|
||||
# Device not found error string
|
||||
ERR_NOTFOUND = "Device not found"
|
||||
|
||||
# Takes a tuple of ints and turns it into bytes with the correct length padded with zeroes
|
||||
def _create_msgdata(self, data: tuple[int]) -> bytes:
|
||||
return bytes(data).ljust(self.MSGLEN, b"0")
|
||||
# Selects correct device, and makes sure we can control it
|
||||
def __init__(self, output_sink=None):
|
||||
# Find HID device path
|
||||
devpath = None
|
||||
for hiddev in hidenumerate(self.VID, self.PID):
|
||||
if hiddev["interface_number"] == self.INTERFACE:
|
||||
devpath = hiddev["path"]
|
||||
break
|
||||
if not devpath:
|
||||
raise DeviceNotFoundException
|
||||
|
||||
# Try to automatically detect output sink, this is skipped if output_sink is given
|
||||
if not output_sink:
|
||||
sinks = (
|
||||
check_output([CMD_PACTL, "list", "sinks", "short"]).decode().split("\n")
|
||||
)
|
||||
for sink in sinks[:-1]:
|
||||
sink_name = sink.split("\t")[1]
|
||||
if self.PW_OUTPUT_SINK_AUTODETECT in sink_name:
|
||||
output_sink = sink_name
|
||||
|
||||
self.dev = device()
|
||||
self.dev.open_path(devpath)
|
||||
self.dev.set_nonblocking(True)
|
||||
self.output_sink = output_sink
|
||||
|
||||
# Enables/Disables chatmix controls
|
||||
def set_chatmix_controls(self, state: bool):
|
||||
assert self.dev, self.ERR_NOTFOUND
|
||||
self.dev.write(
|
||||
self.ENDPOINT_TX,
|
||||
self._create_msgdata((self.TX, self.OPT_CHATMIX_ENABLE, int(state))),
|
||||
)
|
||||
self.CHATMIX_CONTROLS_ENABLED = state
|
||||
|
||||
# Enables/Disables Sonar Icon
|
||||
def set_sonar_icon(self, state: bool):
|
||||
assert self.dev, self.ERR_NOTFOUND
|
||||
self.dev.write(
|
||||
self.ENDPOINT_TX,
|
||||
self._create_msgdata((self.TX, self.OPT_SONAR_ICON, int(state))),
|
||||
)
|
||||
self.SONAR_ICON_ENABLED = state
|
||||
|
||||
# Sets Volume
|
||||
def set_volume(self, attenuation: int):
|
||||
assert self.dev, self.ERR_NOTFOUND
|
||||
self.dev.write(
|
||||
self.ENDPOINT_TX,
|
||||
self._create_msgdata((self.TX, self.OPT_VOLUME, attenuation)),
|
||||
)
|
||||
|
||||
# Sets EQ preset
|
||||
def set_eq_preset(self, preset: int):
|
||||
assert self.dev, self.ERR_NOTFOUND
|
||||
self.dev.write(
|
||||
self.ENDPOINT_TX,
|
||||
self._create_msgdata((self.TX, self.OPT_EQ_PRESET, preset)),
|
||||
)
|
||||
|
||||
# Checks available sinks and select headset
|
||||
def _detect_original_sink(self):
|
||||
# If sink is set manually, skip auto detect
|
||||
if self.PW_ORIGINAL_SINK:
|
||||
return
|
||||
sinks = check_output(["pactl", "list", "sinks", "short"]).decode().split("\n")
|
||||
for sink in sinks:
|
||||
print(sink)
|
||||
name = sink.split("\t")[1]
|
||||
if "SteelSeries_Arctis_Nova_Pro_Wireless" in name:
|
||||
self.PW_ORIGINAL_SINK = name
|
||||
break
|
||||
|
||||
# Creates virtual pipewire loopback sinks, and redirects them to the real headset sink
|
||||
def _start_virtual_sinks(self):
|
||||
self._detect_original_sink()
|
||||
cmd = [
|
||||
"pw-loopback",
|
||||
"-P",
|
||||
self.PW_ORIGINAL_SINK,
|
||||
"--capture-props=media.class=Audio/Sink",
|
||||
"-n",
|
||||
]
|
||||
self.PW_LOOPBACK_GAME_PROCESS = Popen(cmd + [self.PW_GAME_SINK])
|
||||
self.PW_LOOPBACK_CHAT_PROCESS = Popen(cmd + [self.PW_CHAT_SINK])
|
||||
|
||||
def _remove_virtual_sinks(self):
|
||||
self.PW_LOOPBACK_GAME_PROCESS.terminate()
|
||||
self.PW_LOOPBACK_CHAT_PROCESS.terminate()
|
||||
|
||||
# ChatMix implementation
|
||||
# Continuously read from base station and ignore everything but ChatMix messages (OPT_CHATMIX)
|
||||
# The .read method times out and returns an error. This error is catched and basically ignored. Timeout can be configured, but not turned off (I think).
|
||||
def chatmix(self):
|
||||
self._start_virtual_sinks()
|
||||
def chatmix_volume_control(self, chatmix: ChatMix):
|
||||
assert self.dev, self.ERR_NOTFOUND
|
||||
while not self.CLOSE:
|
||||
try:
|
||||
msg = self.dev.read(self.ENDPOINT_RX, self.MSGLEN)
|
||||
if msg[1] != self.OPT_CHATMIX:
|
||||
msg = self.dev.read(self.MSGLEN, self.READ_TIMEOUT)
|
||||
if not msg or msg[1] is not self.OPT_CHATMIX:
|
||||
continue
|
||||
|
||||
# 4th and 5th byte contain ChatMix data
|
||||
gamevol = msg[2]
|
||||
chatvol = msg[3]
|
||||
|
||||
# Set Volume using PulseAudio tools. Can be done with pure pipewire tools, but I didn't feel like it
|
||||
cmd = ["pactl", "set-sink-volume"]
|
||||
|
||||
# Actually change volume. Everytime you turn the dial, both volumes are set to the correct level
|
||||
Popen(cmd + [f"input.{self.PW_GAME_SINK}", f"{gamevol}%"])
|
||||
Popen(cmd + [f"input.{self.PW_CHAT_SINK}", f"{chatvol}%"])
|
||||
# Ignore timeout.
|
||||
except USBTimeoutError:
|
||||
continue
|
||||
except USBError:
|
||||
print("Device was probably disconnected, exiting..")
|
||||
chatmix.set_volumes(gamevol, chatvol)
|
||||
except OSError:
|
||||
print("Device was probably disconnected, exiting.")
|
||||
self.CLOSE = True
|
||||
self._remove_virtual_sinks()
|
||||
# Remove virtual sinks on exit
|
||||
self._remove_virtual_sinks()
|
||||
chatmix.close()
|
||||
|
||||
# Prints output from base station. `debug` argument enables raw output.
|
||||
def print_output(self, debug: bool = False):
|
||||
assert self.dev
|
||||
while not self.CLOSE:
|
||||
try:
|
||||
msg = self.dev.read(self.ENDPOINT_RX, self.MSGLEN)
|
||||
if debug:
|
||||
print(msg)
|
||||
match msg[1]:
|
||||
case self.OPT_VOLUME:
|
||||
print(f"Volume: -{msg[2]}")
|
||||
case self.OPT_CHATMIX:
|
||||
print(f"Game Volume: {msg[2]} - Chat Volume: {msg[3]}")
|
||||
case self.OPT_EQ:
|
||||
print(f"EQ: Bar: {msg[2]} - Value: {(msg[3] - 20) / 2}")
|
||||
case self.OPT_EQ_PRESET:
|
||||
print(f"EQ Preset: {msg[2]}")
|
||||
case _:
|
||||
print("Unknown Message")
|
||||
except USBTimeoutError:
|
||||
continue
|
||||
msg = self.dev.read(self.MSGLEN, self.READ_TIMEOUT)
|
||||
if debug:
|
||||
print(msg)
|
||||
match msg[1]:
|
||||
case self.OPT_VOLUME:
|
||||
print(f"Volume: -{msg[2]}")
|
||||
case self.OPT_CHATMIX:
|
||||
print(f"Game Volume: {msg[2]} - Chat Volume: {msg[3]}")
|
||||
case self.OPT_EQ:
|
||||
print(f"EQ: Bar: {msg[2]} - Value: {(msg[3] - 20) / 2}")
|
||||
case self.OPT_EQ_PRESET:
|
||||
print(f"EQ Preset: {msg[2]}")
|
||||
case _:
|
||||
print("Unknown Message")
|
||||
|
||||
# Terminates processes and disables features
|
||||
def close(self, signum, frame):
|
||||
|
@ -190,14 +205,32 @@ class NovaProWireless:
|
|||
if self.SONAR_ICON_ENABLED:
|
||||
self.set_sonar_icon(False)
|
||||
|
||||
# Takes a tuple of ints and turns it into bytes with the correct length padded with zeroes
|
||||
def _create_msgdata(self, data: tuple[int, ...]) -> bytes:
|
||||
return bytes(data).ljust(self.MSGLEN, b"\0")
|
||||
|
||||
|
||||
class DeviceNotFoundException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# When run directly, just start the ChatMix implementation. (And activate the icon, just for fun)
|
||||
if __name__ == "__main__":
|
||||
nova = NovaProWireless()
|
||||
nova.set_sonar_icon(True)
|
||||
nova.set_chatmix_controls(True)
|
||||
try:
|
||||
nova = NovaProWireless()
|
||||
nova.set_sonar_icon(state=True)
|
||||
nova.set_chatmix_controls(state=True)
|
||||
|
||||
signal(SIGINT, nova.close)
|
||||
signal(SIGTERM, nova.close)
|
||||
signal(SIGINT, nova.close)
|
||||
signal(SIGTERM, nova.close)
|
||||
|
||||
nova.chatmix()
|
||||
assert nova.output_sink, "Output sink not set"
|
||||
chatmix = ChatMix(
|
||||
output_sink=nova.output_sink,
|
||||
main_sink=nova.PW_GAME_SINK,
|
||||
chat_sink=nova.PW_CHAT_SINK,
|
||||
)
|
||||
|
||||
nova.chatmix_volume_control(chatmix=chatmix)
|
||||
except DeviceNotFoundException:
|
||||
print("Device not found, exiting.")
|
||||
|
|
Loading…
Add table
Reference in a new issue