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

Sink Audio Patch #9

Merged
merged 3 commits into from Feb 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -6,3 +6,5 @@
/images/*.xcf

/.idea/

*.pyc
3 changes: 2 additions & 1 deletion config/config_template.yaml
Expand Up @@ -40,7 +40,8 @@ audio:
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)|(VIVE)' # Regex. Used to find the card and port on that card which the vr headset is connected to.
card_port_vr_product_name_regex: '(Index HMD)|(VIVE)' # Regex. Used to find the card and port on that card which the vr headset is connected to.
card_port_normal_product_name_regex: '' # Regex. Used to find the card and port on that card to restore audio to the normal device.
# 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.

Expand Down
77 changes: 41 additions & 36 deletions scripts/audio_switcher.py
Expand Up @@ -7,7 +7,7 @@
import pactl_interface


# tested with pactl version 13.99.1
# tested with pactl version 13.99.1, 14.2

class AudioSwitcher:
class Failure:
Expand Down Expand Up @@ -59,8 +59,6 @@ def __init__(self, config):
self.vr_sink = self.find_matching_sink(sinks, vr_sink_regex, "vr")
log.d('vr sink: {}'.format(self.vr_sink.name))

self.port = None

@staticmethod
def find_matching_sink(sinks, regex, name):
vr_matches = [
Expand All @@ -75,62 +73,57 @@ def find_matching_sink(sinks, regex, name):
raise RuntimeError(
'Multiple matches for the {} audio sink found. Tried to find a match for: {}'.format(name, regex))

def switch_to_sink(self, sink, device_type):
if self.config.audio_set_card_port():
port = self.get_port(device_type)

if port is not None:
port.card.set_profile(self.config, port.profiles[0])
else:
sink.set_suspend_state(self.config, True)
time.sleep(self.config.audio_card_rescan_pause_time())
# Causes a rescan of connected ports, only works if time passes between suspend and resume
sink.set_suspend_state(self.config, False)

self.set_sink_for_all_sink_inputs(sink)

def switch_to_vr(self):
old_vr_sink = self.vr_sink
sinks = pactl_interface.Sink.get_all_sinks(self)
self.vr_sink = self.find_matching_sink(sinks, self.config.audio_vr_sink_regex(), "vr")
if self.vr_sink.name != old_vr_sink.name:
log.d('New vr sink: {}'.format(self.vr_sink.name))

if self.config.audio_set_card_port():
last_port = self.port
self.port = self.get_port()
if last_port != self.port:
log.d('New port: {}'.format(self.port))

if self.port is not None:
self.port.card.set_profile(self.config, self.port.profiles[0])
else:
self.vr_sink.set_suspend_state(self.config, True)
time.sleep(self.config.audio_card_rescan_pause_time())
# Causes a rescan of connected ports, only works if time passes between suspend and resume
self.vr_sink.set_suspend_state(self.config, False)

self.set_sink(self.vr_sink)
self.switch_to_sink(self.vr_sink, "vr")

def switch_to_normal(self):
self.set_sink(self.normal_sink)

def set_sink(self, sink):
# self.set_default_sink(sink)
self.set_sink_for_all_sink_inputs(sink)
self.switch_to_sink(self.normal_sink, "normal")

def log_state(self):
log.d('last_pactl_sinks:\n{}'.format(self.last_pactl_sinks))
log.d('last_pactl_sink_inputs:\n{}'.format(self.last_pactl_sink_inputs))
log.d('last_pactl_clients:\n{}'.format(self.last_pactl_clients))

def set_default_sink(self, sink):
def set_sink_for_all_sink_inputs(self, sink):
if self.config.dry_run():
log.w('Skipping because of dry run')
return

arguments = ['pactl', 'set-default-sink', sink.name]
return_code, stdout, stderr = pactl_interface.utlis.run(arguments)
# verify sink name exists before proceeding
sinks = pactl_interface.Sink.get_all_sinks(self)
found = False
for s in sinks:
if s.name == sink.name:
found = True

if return_code != 0:
log.e('\'{}\' () failed, stderr:\n{}'.format(" ".join(arguments), stderr))
self.log_state()
if not found:
log.w('Skipping move-sink-input since the sink name does not exist')
return

def set_sink_for_all_sink_inputs(self, sink):
sink_inputs = pactl_interface.SinkInput.get_all_sink_inputs(self)
pactl_interface.Client.get_client_names(sink_inputs)
sink_inputs = self.filter_by_client_name(sink_inputs)

if self.config.dry_run():
log.w('Skipping because of dry run')
return

for sink_input in sink_inputs:
failure = \
([failure for failure in self.failed_sink_inputs if failure.sink_input_id == sink_input.id] + [None])[0]
Expand Down Expand Up @@ -170,9 +163,21 @@ def get_default_sink_name():
'Workaround: Fill out the "normal_sink_regex" field in the config file.\n\n'
'Output of `pactl info`:\n{}'.format(stdout))

def get_port(self):
def get_port(self, device_type):
if device_type == "vr":
card_port_product_name_regex = self.config.audio_card_port_vr_product_name_regex()
elif device_type == "normal":
card_port_product_name_regex = self.config.audio_card_port_normal_product_name_regex()
else:
raise NotImplementedError()

if card_port_product_name_regex is None:
log.d(
"Skipping port selection for {device_type} device because card_port_{device_type}_product_name_regex is not set.".format(
device_type=device_type))
return None

cards = pactl_interface.Card.get_all_cards()
card_port_product_name_regex = self.config.audio_card_port_product_name_regex()
for card in cards:
for port in card.ports:
if port.product_name is not None:
Expand Down
15 changes: 14 additions & 1 deletion scripts/config.py
Expand Up @@ -5,6 +5,8 @@

import yaml

import log


class Config:
def __init__(self, config_path=None, dry_run_overwrite=False):
Expand Down Expand Up @@ -152,12 +154,23 @@ def audio_set_card_port(self):

return True

def audio_card_port_product_name_regex(self):
def audio_card_port_vr_product_name_regex(self):
if 'audio' in self.data and 'card_port_vr_product_name_regex' in self.data['audio']:
return self.data['audio']['card_port_vr_product_name_regex']

if 'audio' in self.data and 'card_port_product_name_regex' in self.data['audio']:
log.w(
"Using deprecated config value audio:card_port_product_name_regex, use audio:card_port_vr_product_name_regex instead!")
return self.data['audio']['card_port_product_name_regex']

return '(Index HMD)|(VIVE)'

def audio_card_port_normal_product_name_regex(self):
if 'audio' in self.data and 'card_port_normal_product_name_regex' in self.data['audio']:
return self.data['audio']['card_port_normal_product_name_regex']

return None

def audio_card_rescan_pause_time(self):
if 'audio' in self.data and 'card_rescan_pause_time' in self.data['audio']:
return float(self.data['audio']['card_rescan_pause_time'])
Expand Down
11 changes: 7 additions & 4 deletions scripts/config_helper.py
Expand Up @@ -8,7 +8,7 @@ def __init__(self, config):

def print_help(self):
help_text = '''
# Because regular expressions can often be unintuitive the following output
# Because regular expressions can often be unintuitive the following output
# provides help for configuring steamvr_utils to your setup/preferences.

# Recommended procedure:
Expand Down Expand Up @@ -55,15 +55,18 @@ def print_help(self):
card_port_product_names.append(port.product_name)

help_text += '''
# ==== Help for audio:card_port_product_name_regex ====
# current value for audio:card_port_product_name_regex:
# ==== Help for audio:card_vr_port_product_name_regex and audio:card_normal_port_product_name_regex ====
# current value for audio:card_vr_port_product_name_regex:
{}
# current value for audio:card_normal_port_product_name_regex:
{}
# Exactly one line should match your regex.
# Card port product names: (SteamVR needs to run for the HMD to show up here)
{}

'''.format(
self.config.audio_card_port_product_name_regex(),
self.config.audio_card_port_vr_product_name_regex(),
self.config.audio_card_port_normal_product_name_regex(),
'\n'.join(card_port_product_names)
)

Expand Down