class VolumeCtl(object): def __init__(self): self.pulse = Pulse('volume-controller') self.sinks = self.pulse.sink_list() self.seeed_sink = None for i in self.sinks: if "seeed" in i.name: self.seeed_sink = i break def setVolume(self, vol): if vol > 100: vol = 100 elif vol < 0: vol = 0 vol = vol / 100.0 if self.seeed_sink: try: self.pulse.volume_set_all_chans(self.seeed_sink, vol) except Exception as e: print("Fail to set volume, Error:{}".format(e)) else: print("Fail to find Seeed voicecard") def getVolume(self): if self.seeed_sink: vol = 100.0 * self.pulse.volume_get_all_chans(self.seeed_sink) return vol else: print("Fail to find Seeed voicecard") return (0.0) def setMute(self, muted): if self.seeed_sink: try: self.pulse.mute(self.seeed_sink, muted) except Exception as e: print("Fail to set mute, Error:{}".format(e)) else: print("Fail to find Seeed voicecard")
class PulseMixer(object): profiles = { "analog": None, "a2dp": None, "hsp": None, } current_profile = None active_sink = None def __init__(self): self.pulse = Pulse('volume-control') def introspect(self): for k in self.profiles.keys(): self.profiles[k] = None self.current_profile = None for k in self.profiles.keys(): self.profiles[k] = None self.cards = self.pulse.card_list() self.get_active_sink() for card in self.cards: for profile in card.profile_list: if (card.profile_active.name == profile.name and card.name[:4] == self.active_sink.name[:4]): self.current_profile = profile key = PROFILE_MAP.get(profile.name, None) if key: self.profiles[key] = profile def set_profile(self, key): # traverse through cards to determine which ones to turn on/off next_profile = self.profiles[key] next_card = None for card in self.cards: for profile in card.profile_list: if profile == next_profile: next_card = card break off_card = None off_profile = None for card in self.cards: if card == next_card: continue for profile in card.profile_list: if profile.name == PROFILE_OFF: off_card = card off_profile = profile if not next_card or not next_profile: return self.pulse.card_profile_set(next_card, next_profile) if off_card and off_profile: self.pulse.card_profile_set(off_card, off_profile) def get_active_sink(self): sink = None sink_inputs = self.pulse.sink_input_list() # check if a sink input is connected to a sink, if no, use default if len(sink_inputs): sink_id = sink_inputs[0].sink sinks = self.pulse.sink_list() sink = next((s for s in sinks if s.index == sink_id), None) if sink is None: info = self.pulse.server_info() sink = self.pulse.get_sink_by_name(info.default_sink_name) self.active_sink = sink return self.active_sink def get_sink_volume_and_mute(self): mute = True volume = 0 if self.active_sink: volume = self.pulse.volume_get_all_chans(self.active_sink) volume = min(max(volume, 0), 1) * 100 mute = self.active_sink.mute return volume, mute def set_volume(self, value): if self.active_sink: self.pulse.volume_set_all_chans(self.active_sink, value / 100.0) def change_volume(self, value): if self.active_sink: volume = self.pulse.volume_get_all_chans(self.active_sink) volume += value / 100.0 volume = min(max(volume, 0), 1) self.pulse.volume_set_all_chans(self.active_sink, volume) def toggle_mute(self): sink = self.active_sink sink and self.pulse.mute(sink, not sink.mute) def start_listener(self, func): self.callback = func self.thread = threading.Thread( target=self.async_listener ) self.thread.daemon = True self.thread.start() def async_listener(self): self.pulse_d = Pulse('volume-daemon') self.pulse_d.event_mask_set('sink') # Glib.idle_add is to run the callback in the UI thread self.pulse_d.event_callback_set( lambda e: GLib.idle_add(self.callback) ) self.pulse_d.event_listen()
class Dispatcher: """ A simple dispatcher class that receive powermate events and dispatch them to the right controller """ def __init__(self, observer): """ Initialize the super class and define the local members :param path: The path to the powermate device """ self._long_pressed = False self._pulse = Pulse(threading_lock=True) self._stored_app = None self._display = Display() # Connects to the default display self._note = pynotify.Notification("Volume", "0", "/usr/share/icons/Faenza/apps/48/" "gnome-volume-control.png") self._note.set_urgency(0) self._led = PowermateLed() self._led.max() self._rofi = Rofi() self._rofi.hide_scrollbar = True self._rofi.prompt = "App. name?" def short_press(self): """ Manage the short_press event :return: None """ # Get the list of active sinks sinks = self._get_sinks() # Get the names of the apps linked to the sinks app_sinks = {"{} {}".format(sink.proplist.get("application.name"), sink.index): sink for sink in sinks} if len(app_sinks) > 1: # Display a menu to select the application to control try: res = self._rofi(app_sinks) except MenuError: return app_sink = res.value elif len(app_sinks) == 1: _, app_sink = app_sinks.popitem() else: app_sink = None # If successful if app_sink is not None: # Toggle the mute status of the selected sink self._toggle_mute_sinks([app_sink]) # Declare a new notification self._note.update("Toggle Mute status", "{}".format(app_sink.proplist.get("application.name")), "/usr/share/icons/Faenza/apps/48/gnome-volume-control.png") # Show the notification self._note.show() def long_press(self): """ For the moment this method simply toggles the lights on/off :return: A LedEvent class """ if self._long_pressed: # Re-initialize the state of the powermate self._long_pressed = False self._stored_app = None # Just light up the powermate self._led.max() else: # Get the list of active sinks sinks = self._get_sinks() # Get the names of the apps linked to the sinks app_sinks = {sink.proplist.get("application.name"):sink.proplist.get("application.process.binary") for sink in sinks if sink.proplist.get("application.process.binary") not in self._get_active_win_class()} if len(app_sinks) > 1: # Display a menu to select the application to control try: res = self._rofi(app_sinks) except MenuError: return app_name = res.value elif len(app_sinks) == 1: _, app_name = app_sinks.popitem() else: app_name = None # If successful if app_name is not None: # Store the list of sinks corresponding to the app name self._stored_app = app_name # Toggle the long press state self._long_pressed = True # Have the powermate pulse self._led.pulse() else: # Make sure the long press flag is off self._long_pressed = False # Stop the pulse self._led.max() def rotate(self, rotation): """ Manage the rotate event :param rotation: The direction of rotation negative->left, positive->right :return: None """ # Get the class of the active window win_cls = self._get_active_win_class() if win_cls is not None: # Change the volume of the sinks self._change_volume_sinks(self._get_app_sinks(win_cls), rotation) def push_rotate(self, rotation): """ Changes the volume of the sinks registered by the long_press event, according to the given rotation. :param rotation: The direction and amplitude of the rotation. (negative = left, positive = right). :return: Nothing. """ # Change the volume of the current sinks self._change_volume_sinks(self._get_app_sinks(self._stored_app), rotation) def _toggle_mute_sinks(self, sinks): """ Simply toggle the mute status of all given sinks. :param sinks: A list of sink objects. :return: Nothing. """ # Toggle the mute status for sink in sinks: muted = bool(sink.mute) self._pulse.mute(sink, mute=not muted) def _change_volume_sinks(self, sinks, rotation): """ Simple change the volume of all given sinks and display a notification. :param sinks: A list of sink objects. :param rotation: The amount and direction of the rotation. :return: Nothing. """ # Change the volume of the sinks for sink in sinks: self._pulse.volume_change_all_chans(sink, rotation * 0.005) # Show the notification self._display_notification(sink) def _get_active_win_class(self): """ Use the xlib module to get the class of the window that has the focus :return: Return the window class or None if none found """ # Get the window that has the focus focus_win = self._display.get_input_focus().focus # Get the window class win_cls = focus_win.get_wm_class() if win_cls is None: # Get the class of the parent window parent_cls = focus_win.query_tree().parent.get_wm_class() if parent_cls is not None: return str(parent_cls[-1].lower()) else: return None else: return str(win_cls[-1].lower()) def _get_app_sinks(self, app_name): """ Get the sinks corresponding to the given application :param app_name: Name of the application :return: List of sink objects otherwise. """ # Make sure the app_name is a string if isinstance(app_name, str) and app_name is not None: # Get the list of input sinks sinks = self._get_sinks() # Return the list of sinks corresponding to the application return [sink for sink in sinks if sink.proplist.get("application.process.binary").lower() == app_name] else: return [] def _get_sinks(self): """ Get a list of active pulseaudio sinks :return: List. A list containing all the active sink objects. """ # Get the list of input sinks sinks = [sink for sink in self._pulse.sink_input_list() if sink.proplist.get("application.process.binary", None) is not None] # Return the list of active sinks return sinks def _display_notification(self, sink_in): """ Display a notification showing the overall current volume. :param volume: A float representing the value of the current sink input. :return: Nothing. """ # Get the volume of the input sink volume = self._pulse.volume_get_all_chans(sink_in) # Get the main sink for sink in self._pulse.sink_list(): if sink.index == sink_in.sink: main_vol = sink.volume.value_flat break else: main_vol = 1 # Declare a new notification self._note.update("Volume", "{:.2%}".format(volume * main_vol), "/usr/share/icons/Faenza/apps/48/" "gnome-volume-control.png") # Show the notification self._note.show() def _handle_exception(self): """ Close the connection to the pulse server. :return: Nothing """ # Close the connection to the pulse server self._pulse.close() self._led.off()
class pulseToTalk: """Simple push to talk binding for X / Pulseaudio Work in user mode (command line only) """ def __init__(self, **kwargs): """Init all variables for pulseToTalk and save config Kwargs : - event_code (list) : Bind key names list (optional) (default: None) - no_indicator (bool) : Do not show recording indicator (default: False) - sources (list) : Operate only on the given pulseaudio sources (default: None) - debug (bool) : Activate debug mode (default: False) """ self.config = kwargs # Init logger handler = logging.StreamHandler() handler.setFormatter( colorlog.ColoredFormatter( '[%(name)s]%(log_color)s %(levelname)s > %(message)s')) self.logger = colorlog.getLogger(self.__class__.__name__) self.logger.addHandler(handler) if self.config.get('debug', False) is True: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(logging.WARNING) # Version self.logger.debug('Starting version 1.3') # Create hookmanager self.hookman = pyxhook.HookManager() # Define our callback to fire when a key / mouse is pressed or released self.hookman.KeyDown = self.on_key_event self.hookman.KeyUp = self.on_key_event self.hookman.MouseAllButtonsDown = self.on_key_event self.hookman.MouseAllButtonsUp = self.on_key_event # Hook the keyboard self.hookman.HookKeyboard() # Init pulse (do not support multi-thread !) self.pulse = Pulse(self.__class__.__name__) # Init empty recording indicator self.recording_indicator = None if self.config.get('no_indicator', False) is False: # Init RecordingIndicator self.recording_indicator = RecordingIndicator() # Store if recording is locked self.lock_recording = False # Store if control is keep pushed self.is_control_modifier = False # State of recording self.is_recording = False # Mute all inputs at start self.mute_sources() # Start listener self.hookman.start() # Init custom variables self.stored_event_code = set() # If event code is passed by args if self.config.get('event_code'): self.stored_event_code = set(self.config['event_code']) # Process stored_event_code self.logger.warning('Press CTRL + C to exit...') if len(self.stored_event_code) == 0: self.logger.debug('!!! Key binding not yet configured !') self.logger.critical('Press key/mouse to bind :') else: self.logger.critical('Configured bind(s) : %s' % ', '.join(self.stored_event_code)) def run(self): """Run infinite loop to watch user keys / mouse """ try: # Create a loop to keep the application running while True: if self.recording_indicator is not None: self.recording_indicator.do_update() time.sleep(0.1) except KeyboardInterrupt: pass # Close the listener when we are done self.hookman.cancel() # Unmute all self.mute_sources(False) self.logger.debug('Terminated') def mute_sources(self, is_mute=True): """Mute or unmute all audio sources with pulseaudio """ if is_mute: self.logger.info('Do source(s) MUTE :') else: self.logger.info('Do source(s) UNMUTE :') for source in self.pulse.source_list(): # Exclude monitors input if source.name.endswith('.monitor'): continue # Mute only specified sources if self.config.get( 'sources') and source.name not in self.config.get( 'sources'): continue # Mute source self.pulse.mute(source, is_mute) self.logger.info('- %s' % source) def on_stored_event(self, event): """Switch mode if needed if one of stored event is trigger """ # Lock recording if Ctrl is pressed once with event if self.is_control_modifier is True and self.is_event_down(event): if self.lock_recording is False: self.lock_recording = True else: self.lock_recording = False # If lock_recording force recording until it's stopped # Normal activate recording on press if self.is_recording is False and (self.is_event_down(event) or self.lock_recording is True): self.is_recording = True self.mute_sources(is_mute=False) # Normal disable recording on release key elif self.is_recording is True and self.is_event_up( event) and self.lock_recording is False: self.is_recording = False self.mute_sources() @staticmethod def get_event_code(event): """Return cleaned event code """ # Try to detect if Mouse or Key are pressed if hasattr(event, 'Key') is True: return event.Key.lower() if hasattr(event, 'Position') is True: # Create mouse key without 'up' and 'down' like 'mouse_left' return '_'.join(event.MessageName.lower().split()[0:2]) raise ValueError('Event not recognized : "%s" ' % event) @staticmethod def is_event_down(event): if event is None: return False return event.MessageName.endswith(' down') @staticmethod def is_event_up(event): if event is None: return False return event.MessageName.endswith(' up') def on_key_event(self, event): event_code = self.get_event_code(event) self.logger.debug('Event detected : %s %s' % (event, event_code)) # Store key on first key to be used if no event previously registred if len(self.stored_event_code) == 0: self.stored_event_code.add(event_code) # Print binded event and exit function return self.logger.critical( 'Binded %s event : \'%s\'.' % ('KEY' if hasattr(event, 'Key') else 'MOUSE', event_code)) # Check ctrl modifier key if event_code in ['control_l', 'control_r']: if self.is_event_down(event): self.is_control_modifier = True else: self.is_control_modifier = False # Trigger if event is the same as stored event if event_code in self.stored_event_code: self.on_stored_event(event)
class PulseMixer(object): all_profiles = {} current_profile = None active_sink = None def __init__(self): self.pulse = Pulse('volume-control') def introspect(self): self.all_profiles = {} self.current_profile = None self.cards = self.pulse.card_list() for card in self.cards: description = card.proplist.get('device.description') for profile in card.profile_list: #print(profile) prof_key = PROFILE_MAP.get(profile.name, None) if prof_key and profile.available: key = description + '__' + prof_key self.all_profiles[key] = [card, profile] if (card.profile_active.name == profile.name and self.active_sink and card.name[:4] == self.active_sink.name[:4]): self.current_profile = key def set_profile(self, key): prof = self.all_profiles[key] if not prof: return card, profile = prof for c, p in self.all_profiles.values(): if c != card: self.pulse.card_profile_set(c, PROFILE_OFF) elif p == profile: self.pulse.card_profile_set(card, profile) def get_active_sink(self): sink = None sink_inputs = self.pulse.sink_input_list() # check if a sink input is connected to a sink, if no, use default if len(sink_inputs): sink_id = sink_inputs[0].sink sinks = self.pulse.sink_list() sink = next((s for s in sinks if s.index == sink_id), None) if sink is None: info = self.pulse.server_info() if info.default_sink_name == '@DEFAULT_SINK@': return None sink = self.pulse.get_sink_by_name(info.default_sink_name) self.active_sink = sink return self.active_sink def get_sink_volume_and_mute(self): mute = True volume = 0 if self.active_sink: volume = self.pulse.volume_get_all_chans(self.active_sink) volume = min(max(volume, 0), 1) * 100 mute = self.active_sink.mute return volume, mute def set_volume(self, value): if self.active_sink: self.pulse.volume_set_all_chans(self.active_sink, value / 100.0) def change_volume(self, value): if self.active_sink: volume = self.pulse.volume_get_all_chans(self.active_sink) volume += value / 100.0 volume = min(max(volume, 0), 1) self.pulse.volume_set_all_chans(self.active_sink, volume) def get_mute(self): return self.active_sink.mute def toggle_mute(self): sink = self.active_sink sink and self.pulse.mute(sink, not sink.mute) def start_listener(self, func): self.callback = func self.thread = threading.Thread(target=self.async_listener) self.thread.daemon = True self.thread.start() def async_listener(self): self.pulse_d = Pulse('volume-daemon') self.pulse_d.event_mask_set('sink', 'card') # Glib.idle_add is to run the callback in the UI thread self.pulse_d.event_callback_set(self.callback()) try: self.pulse_d.event_listen() except PulseDisconnected: time.sleep(3) self.pulse = Pulse('volume-control') self.async_listener()
class pulseToTalk(object): """Simple push to talk binding for X / Pulseaudio Work in user mode (command line only) """ def __init__(self, event_code=None, no_indicator=False, debug=False): """Init all variables for pulseToTalk Args : - event_code (list) : Bind key names list (optional) - no_indicator (book) : Do not show recording indicator - debug (bool) : Activate debug mode """ # Init logger handler = logging.StreamHandler() handler.setFormatter( colorlog.ColoredFormatter('%(log_color)s%(message)s')) self.logger = colorlog.getLogger(self.__class__.__name__) self.logger.addHandler(handler) if debug: self.logger.setLevel(logging.DEBUG) else: self.logger.setLevel(logging.WARNING) # Version self.logger.debug('[%s] Starting version 1.2' % (self.__class__.__name__)) # Create hookmanager self.hookman = pyxhook.HookManager() # Define our callback to fire when a key / mouse is pressed or released self.hookman.KeyDown = self.on_key_event self.hookman.KeyUp = self.on_key_event self.hookman.MouseAllButtonsDown = self.on_key_event self.hookman.MouseAllButtonsUp = self.on_key_event # Hook the keyboard self.hookman.HookKeyboard() # Init pulse (do not support multi-thread !) self.pulse = Pulse(self.__class__.__name__) self.no_indicator = no_indicator if self.no_indicator is False: # Init RecordingIndicator self.recording_indicator = RecordingIndicator() # State of recording self.is_recording = False # Mute all inputs at start self.mute_sources() # Start listener self.hookman.start() # Init custom variables self.stored_event_code = set() # If event code is passed by args if event_code: self.stored_event_code = set(event_code) # Process stored_event_code self.logger.warning('> Press CTRL + C to exit...') if len(self.stored_event_code) == 0: self.logger.debug('!!! Key binding not yet configured !') self.logger.critical('> Press key/mouse to bind :') else: self.logger.critical('Configured bind(s) : %s' % ', '.join(self.stored_event_code)) # Finally run self.run() def run(self): """Run infinite loop to watch user keys / mouse """ try: # Create a loop to keep the application running while True: if self.no_indicator is False: self.recording_indicator.do_update() time.sleep(0.1) except KeyboardInterrupt: pass # Close the listener when we are done self.hookman.cancel() # Unmute all self.mute_sources(False) self.logger.debug('[%s] terminated.' % self.__class__.__name__) def mute_sources(self, is_mute=True): """Mute or unmute all audio sources with pulseaudio """ if is_mute: self.logger.info('Do source(s) MUTE :') else: self.logger.info('Do source(s) UNMUTE :') for source in self.pulse.source_list(): # Exclude monitors input if source.name.endswith('.monitor'): continue # Mute source self.pulse.mute(source, is_mute) self.logger.info('- %s' % source) def on_stored_event(self, event): """Switch mode if needed if one of stored event is trigger """ if not self.is_recording and event.MessageName.endswith(' down'): self.is_recording = True self.mute_sources(is_mute=False) elif self.is_recording and event.MessageName.endswith(' up'): self.is_recording = False self.mute_sources() def get_event_code(self, event): """Return cleaned event code """ if isinstance(event, pyxhook.pyxhookkeyevent): return event.Key.lower() if isinstance(event, pyxhook.pyxhookmouseevent): # Create mouse key without 'up' and 'down' like 'mouse_left' return '_'.join(event.MessageName.lower().split()[0:2]) raise ValueError('Event not recognized : "%s" ' % event) def on_key_event(self, event): event_code = self.get_event_code(event) self.logger.debug('Event detected : %s' % event_code) # Store key on first key to be used if no event previously registred if len(self.stored_event_code) == 0: self.stored_event_code.add(event_code) # Print binded event and exit function return self.logger.critical( 'Binded %s event : \'%s\'.' % ('KEY' if isinstance( event, pyxhook.pyxhookkeyevent) else 'MOUSE', event_code)) # Trigger if event is the same as stored event if event_code in self.stored_event_code: self.on_stored_event(event)
sink = pulse.sink_list()[SINK_ID] active_port = sink.port_active.name volume = round(sink.volume.value_flat * 100) is_muted = sink.mute == 1 args = args_parser() if args.PORT: if active_port == SPEAKERS: pulse.port_set(sink, HEADPHONES) else: pulse.port_set(sink, SPEAKERS) if args.MUTE: if is_muted: pulse.mute(sink, mute=False) else: pulse.mute(sink, mute=True) if args.UP: if volume < 150: pulse.volume_change_all_chans(sink, +0.05) volume = round(sink.volume.value_flat * 100) if args.DOWN: pulse.volume_change_all_chans(sink, -0.05) volume = round(sink.volume.value_flat * 100) volume = round(sink.volume.value_flat * 100) if not is_muted: if active_port == SPEAKERS: