From 30b08581d2bb2c961576d91aad33cd159813da71 Mon Sep 17 00:00:00 2001 From: Casey DeLorme Date: Sat, 13 Feb 2021 22:21:22 -0500 Subject: [PATCH 1/3] add normal port regex to restore add pre-flight check to avoid move-sink-input when sink name does not exist add port/profile step to switch_to_normal to restore audio --- .gitignore | 3 ++ config/config_template.yaml | 3 +- scripts/audio_switcher.py | 71 +++++++++++++++++++++++++------------ scripts/config.py | 12 +++++-- scripts/config_helper.py | 4 +-- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 86b4f7d..7089551 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ /images/*.xcf /.idea/ + +*.pyc + diff --git a/config/config_template.yaml b/config/config_template.yaml index cefc90a..01312e2 100644 --- a/config/config_template.yaml +++ b/config/config_template.yaml @@ -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. # 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. diff --git a/scripts/audio_switcher.py b/scripts/audio_switcher.py index 804cbfe..8269650 100755 --- a/scripts/audio_switcher.py +++ b/scripts/audio_switcher.py @@ -6,8 +6,7 @@ import log import pactl_interface - -# tested with pactl version 13.99.1 +# tested with pactl version 14.2 class AudioSwitcher: class Failure: @@ -84,7 +83,7 @@ def switch_to_vr(self): if self.config.audio_set_card_port(): last_port = self.port - self.port = self.get_port() + self.port = self.get_vr_port() if last_port != self.port: log.d('New port: {}'.format(self.port)) @@ -96,33 +95,39 @@ def switch_to_vr(self): # 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.set_sink_for_all_sink_inputs(self.vr_sink) def switch_to_normal(self): - self.set_sink(self.normal_sink) + if self.config.audio_set_card_port(): + self.port = self.get_normal_port() - def set_sink(self, sink): - # self.set_default_sink(sink) - self.set_sink_for_all_sink_inputs(sink) + if self.port is not None: + self.port.card.set_profile(self.config, self.port.profiles[0]) + else: + self.normal_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.normal_sink.set_suspend_state(self.config, False) + + self.set_sink_for_all_sink_inputs(self.normal_sink) 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): - 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) + def set_sink_for_all_sink_inputs(self, sink): + # 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.d('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) @@ -170,13 +175,33 @@ 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_normal_port(self): + cards = pactl_interface.Card.get_all_cards() + card_port_normal_product_name_regex = self.config.audio_card_port_normal_product_name_regex() + for card in cards: + for port in card.ports: + if port.product_name is not None: + if re.match(card_port_normal_product_name_regex, port.product_name): + return port + + debug_output = '' + for card in cards: + debug_output += card.name + '\n' + for port in card.ports: + debug_output += ' {}\n'.format(port.product_name if port.product_name is not None else '-') + log.w('Failed to find any port on any card matching "{}". Name of the product at every port:\n{}'.format( + card_port_normal_product_name_regex, debug_output + )) + + return None + + def get_vr_port(self): cards = pactl_interface.Card.get_all_cards() - card_port_product_name_regex = self.config.audio_card_port_product_name_regex() + card_port_vr_product_name_regex = self.config.audio_card_port_vr_product_name_regex() for card in cards: for port in card.ports: if port.product_name is not None: - if re.match(card_port_product_name_regex, port.product_name): + if re.match(card_port_vr_product_name_regex, port.product_name): return port debug_output = '' @@ -185,7 +210,7 @@ def get_port(self): for port in card.ports: debug_output += ' {}\n'.format(port.product_name if port.product_name is not None else '-') log.w('Failed to find any port on any card matching "{}". Name of the product at every port:\n{}'.format( - card_port_product_name_regex, debug_output + card_port_vr_product_name_regex, debug_output )) return None diff --git a/scripts/config.py b/scripts/config.py index 113a86a..6950911 100755 --- a/scripts/config.py +++ b/scripts/config.py @@ -152,12 +152,18 @@ def audio_set_card_port(self): return True - def audio_card_port_product_name_regex(self): - if 'audio' in self.data and 'card_port_product_name_regex' in self.data['audio']: - return self.data['audio']['card_port_product_name_regex'] + 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'] 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']) diff --git a/scripts/config_helper.py b/scripts/config_helper.py index 0ba6527..bca93e6 100755 --- a/scripts/config_helper.py +++ b/scripts/config_helper.py @@ -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: @@ -63,7 +63,7 @@ def print_help(self): {} '''.format( - self.config.audio_card_port_product_name_regex(), + self.config.audio_card_port_vr_product_name_regex(), '\n'.join(card_port_product_names) ) From 0348a75cec5dca1010a28236c9b9570f1839940c Mon Sep 17 00:00:00 2001 From: David Risch Date: Sun, 14 Feb 2021 14:54:55 +0100 Subject: [PATCH 2/3] Improve backwards compatibility and refactor --- .gitignore | 1 - config/config_template.yaml | 2 +- scripts/audio_switcher.py | 94 +++++++++++++++---------------------- scripts/config.py | 7 +++ scripts/config_helper.py | 7 ++- 5 files changed, 50 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index 7089551..ef00848 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ /.idea/ *.pyc - diff --git a/config/config_template.yaml b/config/config_template.yaml index 01312e2..a992bb6 100644 --- a/config/config_template.yaml +++ b/config/config_template.yaml @@ -41,7 +41,7 @@ audio: - 'firefox' set_card_port: true # Boolean. Enable automatic changing the correct port. 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. + 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. diff --git a/scripts/audio_switcher.py b/scripts/audio_switcher.py index 8269650..c40d6b4 100755 --- a/scripts/audio_switcher.py +++ b/scripts/audio_switcher.py @@ -6,7 +6,8 @@ import log import pactl_interface -# tested with pactl version 14.2 + +# tested with pactl version 13.99.1, 14.2 class AudioSwitcher: class Failure: @@ -58,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 = [ @@ -74,42 +73,31 @@ 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_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)) - + def switch_to_sink(self, sink, device_type): if self.config.audio_set_card_port(): - last_port = self.port - self.port = self.get_vr_port() - if last_port != self.port: - log.d('New port: {}'.format(self.port)) + port = self.get_port(device_type) - if self.port is not None: - self.port.card.set_profile(self.config, self.port.profiles[0]) + if port is not None: + port.card.set_profile(self.config, port.profiles[0]) else: - self.vr_sink.set_suspend_state(self.config, True) + 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) + sink.set_suspend_state(self.config, False) - self.set_sink_for_all_sink_inputs(self.vr_sink) + self.set_sink_for_all_sink_inputs(sink) - def switch_to_normal(self): - if self.config.audio_set_card_port(): - self.port = self.get_normal_port() + 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.port is not None: - self.port.card.set_profile(self.config, self.port.profiles[0]) - else: - self.normal_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.normal_sink.set_suspend_state(self.config, False) + self.switch_to_sink(self.vr_sink, "vr") - self.set_sink_for_all_sink_inputs(self.normal_sink) + def switch_to_normal(self): + self.switch_to_sink(self.normal_sink, "normal") def log_state(self): log.d('last_pactl_sinks:\n{}'.format(self.last_pactl_sinks)) @@ -117,6 +105,10 @@ def log_state(self): log.d('last_pactl_clients:\n{}'.format(self.last_pactl_clients)) def set_sink_for_all_sink_inputs(self, sink): + if self.config.dry_run(): + log.w('Skipping because of dry run') + return + # verify sink name exists before proceeding sinks = pactl_interface.Sink.get_all_sinks(self) found = False @@ -125,17 +117,13 @@ def set_sink_for_all_sink_inputs(self, sink): found = True if not found: - log.d('Skipping move-sink-input since the sink name does not exist') + log.w('Skipping move-sink-input since the sink name does not exist') return 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] @@ -175,33 +163,25 @@ 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_normal_port(self): - cards = pactl_interface.Card.get_all_cards() - card_port_normal_product_name_regex = self.config.audio_card_port_normal_product_name_regex() - for card in cards: - for port in card.ports: - if port.product_name is not None: - if re.match(card_port_normal_product_name_regex, port.product_name): - return port - - debug_output = '' - for card in cards: - debug_output += card.name + '\n' - for port in card.ports: - debug_output += ' {}\n'.format(port.product_name if port.product_name is not None else '-') - log.w('Failed to find any port on any card matching "{}". Name of the product at every port:\n{}'.format( - card_port_normal_product_name_regex, debug_output - )) + 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() - return None + 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 - def get_vr_port(self): cards = pactl_interface.Card.get_all_cards() - card_port_vr_product_name_regex = self.config.audio_card_port_vr_product_name_regex() for card in cards: for port in card.ports: if port.product_name is not None: - if re.match(card_port_vr_product_name_regex, port.product_name): + if re.match(card_port_product_name_regex, port.product_name): return port debug_output = '' @@ -210,7 +190,7 @@ def get_vr_port(self): for port in card.ports: debug_output += ' {}\n'.format(port.product_name if port.product_name is not None else '-') log.w('Failed to find any port on any card matching "{}". Name of the product at every port:\n{}'.format( - card_port_vr_product_name_regex, debug_output + card_port_product_name_regex, debug_output )) return None diff --git a/scripts/config.py b/scripts/config.py index 6950911..d4cc1fa 100755 --- a/scripts/config.py +++ b/scripts/config.py @@ -5,6 +5,8 @@ import yaml +import log + class Config: def __init__(self, config_path=None, dry_run_overwrite=False): @@ -156,6 +158,11 @@ 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): diff --git a/scripts/config_helper.py b/scripts/config_helper.py index bca93e6..d5d929e 100755 --- a/scripts/config_helper.py +++ b/scripts/config_helper.py @@ -55,8 +55,10 @@ 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) @@ -64,6 +66,7 @@ def print_help(self): '''.format( self.config.audio_card_port_vr_product_name_regex(), + self.config.audio_card_normal_port_product_name_regex(), '\n'.join(card_port_product_names) ) From 6f86116ee8cd1253183cf9da64c5455f897b16d4 Mon Sep 17 00:00:00 2001 From: Casey DeLorme Date: Sun, 14 Feb 2021 11:06:31 -0500 Subject: [PATCH 3/3] fix typo in function reference --- scripts/config_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/config_helper.py b/scripts/config_helper.py index d5d929e..1e1d751 100755 --- a/scripts/config_helper.py +++ b/scripts/config_helper.py @@ -66,7 +66,7 @@ def print_help(self): '''.format( self.config.audio_card_port_vr_product_name_regex(), - self.config.audio_card_normal_port_product_name_regex(), + self.config.audio_card_port_normal_product_name_regex(), '\n'.join(card_port_product_names) )