Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues with audio automatic switching #5

Closed
Firestorm7893 opened this issue Jan 17, 2021 · 8 comments
Closed

Issues with audio automatic switching #5

Firestorm7893 opened this issue Jan 17, 2021 · 8 comments

Comments

@Firestorm7893
Copy link

Sorry for bugging you again, but I'm having some troubles with the audio component.
I adapted your script to work with the Vive lighthouses so I got that part figured out, what's missing for this setup to be perfect is the automatic audio switch.
For sure i configured something wrong (even though the debug strings it prints makes it look like it chose the right sinks) but whenever i start VrUtils it crashes with this error:

firestorm7893@firestorm7893-desktop:~/.steamvr_utils/scripts$ ./steamvr_utils.py on
2021-01-17 20:01:36 [DEBUG]: dry_run: False
2021-01-17 20:01:36 [DEBUG]: 'pactl list short sinks':
0	alsa_output.pci-0000_01_00.1.hdmi-stereo	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED
1	alsa_output.usb-Corsair_Corsair_VOID_PRO_Wireless_Gaming_Headset-00.analog-stereo	module-alsa-card.c	s16le 2ch 44100Hz	IDLE
2	alsa_output.pci-0000_00_1f.3.analog-stereo	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED
4	PulseEffects_apps	module-null-sink.c	s16le 2ch 44100Hz	IDLE
5	PulseEffects_mic	module-null-sink.c	s16le 2ch 44100Hz	SUSPENDED

2021-01-17 20:01:36 [DEBUG]: normal sink: alsa_output.usb-Corsair_Corsair_VOID_PRO_Wireless_Gaming_Headset-00.analog-stereo
2021-01-17 20:01:36 [DEBUG]: vr sink: alsa_output.pci-0000_01_00.1.hdmi-stereo
2021-01-17 20:01:36 [INFO ]: SteamvrUtils turning on:
2021-01-17 20:01:36 [ERROR]: 
Traceback (most recent call last):
  File "./steamvr_utils.py", line 110, in main
    steamvr_utils.action(selected_action)
  File "./steamvr_utils.py", line 35, in action
    self.turn_on()
  File "./steamvr_utils.py", line 63, in turn_on
    self.audio_switcher.switch_to_vr()
  File "/home/firestorm7893/.steamvr_utils/scripts/audio_switcher.py", line 87, in switch_to_vr
    self.port = self.get_port()
  File "/home/firestorm7893/.steamvr_utils/scripts/audio_switcher.py", line 215, in get_port
    cards = pactl_interface.Card.get_all_cards()
  File "/home/firestorm7893/.steamvr_utils/scripts/pactl_interface/card.py", line 157, in get_all_cards
    cards.append(cls(card_dict))
  File "/home/firestorm7893/.steamvr_utils/scripts/pactl_interface/card.py", line 86, in __init__
    self.ports.append(self.Port(port_dict, self))
  File "/home/firestorm7893/.steamvr_utils/scripts/pactl_interface/card.py", line 31, in __init__
    self.name = match.group(1)
AttributeError: 'NoneType' object has no attribute 'group'

I'll also add my current configuration:

# copy to config.yaml to customize

dry_run: false  # Boolean. Do not change anything.

log:
  enabled: true  # Boolean. Write a log for each execution.

basestation:
  enabled: false  # Boolean. Enable the Base Station component.
  attempt_count_scan: 5  # Int >0. Maximum number of attempts to find Base Stations.
  attempt_count_set: 5  # Int >0. Number of attempts to set the power state of Base Station.

audio:
  enabled: true  # Boolean. Enable the Base Station component.
  vr_sink_regex: '.*hdmi.*'  # Regex. Used to find the audio sink of the vr headset.
  normal_sink_regex: ''  # Regex. Used to find the audio sink of the regular audio device. Leave empty to detect automatically (not recomme>
  excluded_clients_regexes: # List of regexes. Used to ignore some audio clients. Intended for application not needed in vr.
    - 'firefox'
  set_card_port: true  # Boolean. Enable automatic changing the correct port.
  card_port_product_name_regex: 'Valve VR Radio & HMD Mic'  # Regex. Used to find the card and port on that card which the vr headset is co>
  # Use `pactl list cards | grep 'device.product.name'` while SteamVR is running to find the name of your vr headset.
  card_rescan_pause_time: 10  # Float. Number of seconds to wait between suspending and resuming a sink to rescan the ports of its card.

daemon:
  watch_process_name: 'vrcompositor'  # String. Name of the process which indicated SteamVR is running.
  wait_after_quit: 60  # Float. Number of seconds to wait after SteamVR exits until Base Stations are turned off (and audio is switched). U>

@DavidRisch
Copy link
Owner

I adapted your script to work with the Vive lighthouses

Great! Could you please submit a pull request so others can also use V1 Base Stations?

There are two separate problems:

  • Your value of card_port_product_name_regex looks wrong. That is the name of the microphone. For some reason that is completely separate device (at least on the Index, might be different for other headsets). Did you run pactl list cards | grep 'device.product.name' while SteamVR was running?
  • There is some bug in my parser, causing the error message. Please send me the output of ./scripts/debug_dump.py.

@Firestorm7893
Copy link
Author

Yes i noticed, I switched "Valve VR Radio with" "Index hmd".

I tried messing around with the sink regex option, but my head imploded when I saw all the parameters of regex

output.txt
This is the output of the debug script.

Regarding the pull request, it's still a really hacky solution, I basically spawn a subprocess of python running the script you sent me (https://github.com/risa2000/lhctrl) and kill it once the daemon sees steamvr has closed. I can try cleaning it up a bit, but I'm no expert by any means. 😅

@DavidRisch
Copy link
Owner

If you didn't change anything with the lhctl script, it probably makes more sense if I integrate it myself. Can you please send me your current state as a patch file (you do not need to clean it up)? I will inegrate it properly and get back to you for testing in a few days.

Thanks for the output.

@DavidRisch
Copy link
Owner

The error should now be fixed by 6f42c6a.

@Firestorm7893
Copy link
Author

i cleaned my code a bit, what it's missing really is reading the values at the top from the config.

--- a/basestation_interface.py	2021-01-17 22:49:44.000000000 +0100
+++ b/basestation_interface.py	2021-01-18 11:32:09.028425343 +0100
@@ -1,15 +1,21 @@
 import enum
 import time
-
+import subprocess
+import os
+import sys
 # sudo apt install pip3 libglib2.0-dev
 # sudo pip3 install bluepy
 import bluepy
-
+import signal
 import log
-
+import psutil
 
 # based on: https://gist.github.com/waylonflinn/d525e08674ec3abb5c98cd41d1fd2f24
-
+STATIONVER="1.0"
+BMAC = "40:4E:36:BD:A8:45" #address of the station (b)
+BID  = "62F5DDD9" #id found on the rear of the base station (b)
+CMAC = "40:4E:36:BD:A1:38" #address of the station (c)
+CID  = "0E97C927" #id found on the rear of the base station (c)
 
 class BasestationPowerInterface:
     class Action(enum.Enum):
@@ -19,8 +25,10 @@
 
     def __init__(self, config):
         self.config = config
-
+        self.vivesub = None
         self.devices = []
+        self.active = False
+        self.count = 0
 
     def scan(self):
         class Delegate(bluepy.btle.DefaultDelegate):
@@ -63,14 +71,53 @@
                 raise e
 
         self.devices = delegate.devices
-        if len(self.devices) == 0:
-            raise RuntimeError('Bluetooth scan found no Base Stations. '
-                               'If there are powered Base Stations near you, '
-                               'this is probably a problem with your Bluetooth device.')
 
     def action(self, action):
         address = 0x12  # location of the byte which sets the power state
 
+        if STATIONVER=="1.0":
+            if action == self.Action.TOGGLE:
+                if self.active:
+                    action = self.action.OFF
+                else:
+                    action = self.action.ON
+
+            if action == self.Action.ON:
+                proc = []
+                for p in psutil.process_iter():
+                    for j in p.cmdline():
+                        if 'lhctrl.py' in j:
+                            proc.append(p)
+                
+                if len(proc) >= 1:
+                    self.active = True
+                    return
+
+                if not self.active:
+                    self.vivesub = subprocess.Popen([sys.executable,os.path.join(sys.path[0],"lhctrl.py"),"-b " + BID, "--lh_b_mac",BMAC,"-c " + CID, "--lh_c_mac",CMAC])         
+                    self.active = True
+            
+            if action == self.Action.OFF:
+                #if self.active:                                     #lhctrl.py works by pinging the base stations every couple of seconds, so by opening a process with the script
+                #    if self.vivesub.poll() != None:                 #the base stations will turn on, and will stay on unless lhctrl.py is closed.
+                #        self.vivesub.send_signal(signal.SIGINT)     #if lhctrl crashes (Which could happen if doesn't find one of the stations) the lighthouse which got turned on will
+                #                                                    #shut off auomatically.
+                #    self.active = False                             #if lhctrl receives a SIGINT signal (equivalent to ctrl+c) it will ping the stations once more setting their ping timeout to 0
+                #else:                                               #and will shutdown instantly. 
+                self.active = False        
+                proc = []                                               
+                for p in psutil.process_iter():
+                    for j in p.cmdline():
+                        if 'lhctrl.py' in j:
+                            proc.append(p)
+                
+                for p in proc:
+                    print("Terminating")
+                    p.send_signal(signal.SIGINT)
+                time.sleep(10)                                                   
+            return
+        
+
         for device in self.devices:
             basestation = bluepy.btle.Peripheral()
             log.i('Connecting to {}'.format(device))
@@ -88,13 +135,17 @@
 
             if action == self.Action.ON:
                 if not self.config.dry_run():
-                    basestation.writeCharacteristic(address, b'\x01')
+
+                        basestation.writeCharacteristic(address, b'\x01')
                 else:
                     log.w('Skipping because of dry run:')
                 log.i('Turning on')
             elif action == self.Action.OFF:
                 if not self.config.dry_run():
-                    basestation.writeCharacteristic(address, b'\x00')
+                    if STATIONVER=="1.0":
+                        self.vivesub.kill()
+                    else:
+                        basestation.writeCharacteristic(address, b'\x00')
                 else:
                     log.w('Skipping because of dry run:')
                 log.i('Turning off')
@@ -106,6 +157,8 @@
             attempt_count = 0
             success_count = 0
             last_error = None
+            if STATIONVER=="1.0":
+                max_attempts = 1
             while attempt_count < max_attempts:
                 try:
                     if try_all:
@@ -116,7 +169,7 @@
                     log.i('Success of attempt {} of {}'.format(attempt_count + 1, max_attempts))
                 except Exception as e:
                     last_error = e
-                    log.e('Failure of attempt {} of {}: {}'.format(attempt_count + 1, max_attempts, e))
+                    log.e('Failure of attempt {} of {}'.format(attempt_count + 1, max_attempts))
                 attempt_count += 1
 
                 time.sleep(0.5)  # to increase robustness
@@ -124,8 +177,8 @@
             if success_count == 0:
                 log.e('No successful attempt in any of the {} attempts. Last error:'.format(max_attempts))
                 raise last_error
-
-        log.i("Scanning for Base Stations:")
-        attempt_loop(lambda: self.scan(), self.config.basestation_attempt_count_scan())
-        log.i("Changing power state of Base Stations:")
+        
+        if STATIONVER != "1.0":
+            attempt_loop(lambda: self.scan(), self.config.basestation_attempt_count_scan())
+        
         attempt_loop(lambda: self.action(action), self.config.basestation_attempt_count_set(), try_all=True)

patch.txt

@Firestorm7893
Copy link
Author

I'm still having issues with the audio sink. Maybe i set wrong the value for regex?
This is the error I'm getting now:

firestorm7893@firestorm7893-desktop:~$ cat .steamvr_utils/log/latest.log 
2021-01-18 18:44:47 [DEBUG]: dry_run: False
2021-01-18 18:44:47 [DEBUG]: 'pactl list short sinks':
1	alsa_output.usb-Corsair_Corsair_VOID_PRO_Wireless_Gaming_Headset-00.analog-stereo	module-alsa-card.c	s16le 2ch 44100Hz	RUNNING
3	PulseEffects_apps	module-null-sink.c	s16le 2ch 44100Hz	RUNNING
4	PulseEffects_mic	module-null-sink.c	s16le 2ch 44100Hz	SUSPENDED
6	alsa_output.pci-0000_00_1f.3.analog-stereo	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED
79	alsa_output.pci-0000_01_00.1.hdmi-stereo	module-alsa-card.c	s16le 2ch 44100Hz	SUSPENDED

2021-01-18 18:44:47 [DEBUG]: normal sink: alsa_output.usb-Corsair_Corsair_VOID_PRO_Wireless_Gaming_Headset-00.analog-stereo
2021-01-18 18:44:47 [ERROR]: No vr audio sink for the normal device was found. Tried to find a match for: .*1.hdmi-stereo-extra.*
2021-01-18 18:44:47 [ERROR]: 
Traceback (most recent call last):
  File "/home/firestorm7893/.steamvr_utils/scripts/steamvr_utils.py", line 101, in main
    steamvr_utils = SteamvrUtils(
  File "/home/firestorm7893/.steamvr_utils/scripts/steamvr_utils.py", line 31, in __init__
    self.audio_switcher = AudioSwitcher(config)
  File "/home/firestorm7893/.steamvr_utils/scripts/audio_switcher.py", line 60, in __init__
    log.d('vr sink: {}'.format(self.vr_sink.name))
AttributeError: 'NoneType' object has no attribute 'name'

And this is my current config:

firestorm7893@firestorm7893-desktop:~$ cat .steamvr_utils/config/config.yaml 
# copy to config.yaml to customize

dry_run: false  # Boolean. Do not change anything.

log:
  enabled: true  # Boolean. Write a log for each execution.

basestation:
  enabled: true  # Boolean. Enable the Base Station component.
  attempt_count_scan: 5  # Int >0. Maximum number of attempts to find Base Stations.
  attempt_count_set: 5  # Int >0. Number of attempts to set the power state of Base Station.

audio:
  enabled: true  # Boolean. Enable the Base Station component.
  vr_sink_regex: '.*1.hdmi-stereo-extra.*'  # Regex. Used to find the audio sink of the vr headset.
  normal_sink_regex: ''  # Regex. Used to find the audio sink of the regular audio device. Leave empty to detect automatically (not recommended).
  excluded_clients_regexes: # List of regexes. Used to ignore some audio clients. Intended for application not needed in vr.
    - 'firefox'
  set_card_port: true  # Boolean. Enable automatic changing the correct port.
  card_port_product_name_regex: 'Index HMD'  # Regex. Used to find the card and port on that card which the vr headset is connected to.
  # Use `pactl list cards | grep 'device.product.name'` while SteamVR is running to find the name of your vr headset.
  card_rescan_pause_time: 10  # Float. Number of seconds to wait between suspending and resuming a sink to rescan the ports of its card.

daemon:
  watch_process_name: 'vrcompositor'  # String. Name of the process which indicated SteamVR is running.
  wait_after_quit: 60  # Float. Number of seconds to wait after SteamVR exits until Base Stations are turned off (and audio is switched). Useful to prevent a power cycle when restarting SteamVR.

@DavidRisch
Copy link
Owner

Is there any reason why you changed vr_sink_regex from its default value of .*hdmi.*?
You need a regex to match alsa_output.pci-0000_01_00.1.hdmi-stereo, the default value should do that.

This seems to be less intuative than I assumed, I will add some more documentation.

@Firestorm7893
Copy link
Author

I forgot why i changed it. I put it back to the default value and it works perfectly.
Wow I'm dumb.
Problem solved :) thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants