diff --git a/.gitignore b/.gitignore index 86b4f7d..ef00848 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /images/*.xcf /.idea/ + +*.pyc diff --git a/config/config_template.yaml b/config/config_template.yaml index cefc90a..a992bb6 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 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 804cbfe..c40d6b4 100755 --- a/scripts/audio_switcher.py +++ b/scripts/audio_switcher.py @@ -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: @@ -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 = [ @@ -75,6 +73,20 @@ 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) @@ -82,55 +94,36 @@ def switch_to_vr(self): 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] @@ -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: diff --git a/scripts/config.py b/scripts/config.py index 113a86a..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): @@ -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']) diff --git a/scripts/config_helper.py b/scripts/config_helper.py index 0ba6527..1e1d751 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: @@ -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) )