コード例 #1
0
ファイル: pa.py プロジェクト: pohmelie/patray
class Pulse:
    def __init__(self, config):
        self.config = config
        self.pa = PulseOriginal("patray")

    def close(self):
        self.pa.close()

    def update(self):
        logger.debug("update called")
        self.cards = [
            Card.from_pa_card(id=i, pa_card=c)
            for i, c in enumerate(self.pa.card_list())
        ]
        ends = chain(self.pa.sink_list(), self.pa.source_list())
        self.ends = [
            End.from_pa_end(id=i, pa_end=e) for i, e in enumerate(ends)
        ]

    def set_profile(self, profile: CardProfile):
        card = self.cards[profile.card_id]
        self.pa.card_profile_set(card.original, profile.original)
        logger.info("set profile {!r} on card {!r}", profile.name, card.name)

    def set_port(self, port: Port):
        end = self.ends[port.end_id]
        self.pa.port_set(end.original, port.original)
        logger.info("set port {!r} on end {!r}", port.name, end.name)

    def set_volume(self, end: End, volume: float):
        self.pa.volume_set_all_chans(end.original, volume)
        logger.info("set volume {} on end {!r}", volume, end.name)
コード例 #2
0
ファイル: volume_tray.py プロジェクト: Zolmeister/applets
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()
コード例 #3
0
def main(args):
    pulse = Pulse("pulse-audio-cycle")

    current_sink = pulse.get_sink_by_name(
        pulse.server_info().default_sink_name)

    current_profile = pulse.card_info(current_sink.card).profile_active

    # card -> holds all profiles and sets the active one
    # sink uses a card+profile combination and is names accordingly

    matching_cards_with_profiles = []
    card_pattern = re.compile(args.card)
    logging.debug(f"card_pattern: {card_pattern}")
    # Get a list of all matching cards
    for card in pulse.card_list():
        # Prepare to also match against sink description
        sink_for_current_card = None
        if args.use_sink_description:
            sink_for_current_card = sink_for_card(card, pulse)

        card_pattern_matched = False
        if re.search(card_pattern, card.name):
            card_pattern_matched = True
            logging.info(f"Card matched: {card.name}")
            if sink_for_current_card:
                logging.info(
                    f"-> Sink Description: {sink_for_current_card.description}"
                )
        elif args.use_sink_description:
            if sink_for_current_card:
                if re.search(card_pattern, sink_for_current_card.description):
                    card_pattern_matched = True
                    logging.info(f"Card matched: {card.name}")
                    logging.info(
                        f"-> Sink Description: {sink_for_current_card.description}"
                    )
                    logging.info("-> matched via Sink Description")
        # Ignore cards that are not wanted by the user given pattern
        if not card_pattern_matched:
            continue

        matched_profiles = []
        # Check if we need filter for certain profiles or leave as is
        for profile in card.profile_list:
            # skip unavailable profiles (unless wanted)
            if not profile.available and not args.with_unavailable:
                continue

            # Check every given profile
            for cp_card_pattern, cp_profile_pattern in args.profile:
                cp_card_pattern_matched = False
                if re.search(cp_card_pattern, card.name):
                    cp_card_pattern_matched = True
                elif args.use_sink_description:
                    if sink_for_current_card:
                        if re.search(cp_card_pattern,
                                     sink_for_current_card.description):
                            cp_card_pattern_matched = True

                # This cp_profile_pattern does not apply to this card
                if not cp_card_pattern_matched:
                    continue

                if re.search(cp_profile_pattern, profile.name):
                    logging.info(f"  Profile matched: {profile.name} ")
                    matched_profiles.append(profile)

        if not matched_profiles:
            logging.info("  No Profile matched – Keeping profile.")

        # put infos into list
        matching_cards_with_profiles.append((card, matched_profiles))

        # separator betweem cards
        logging.info("")

    new_card, new_profile = new_card_and_profile(matching_cards_with_profiles,
                                                 pulse)

    # change profile if necessary
    if new_profile:
        if args.verbose:
            logging.info(f"New Profile: {new_profile.description}")
        if not args.dry:
            pulse.card_profile_set(new_card, new_profile)
    else:
        if args.verbose:
            logging.info("NO new Profile.")

    # change sink (has to be done always because card profile also changes sink)
    new_sink = sink_for_card(new_card, pulse)
    if args.verbose:
        logging.info(f"New Card: {new_card.name}")
        if args.use_sink_description:
            logging.info(f"-> New Sink: {new_sink.description} ")

    if not args.dry:
        pulse.sink_default_set(new_sink)

    # move all input sinks (apps/X clients) to new output sink
    for input_sink in pulse.sink_input_list():
        if args.verbose:
            logging.info(
                f"  -> Switching {input_sink.proplist['application.name']}")
        if not args.dry:
            pulse.sink_input_move(input_sink.index, new_sink.index)

    # Show notification
    if args.notify:
        details = f"New Sink: {new_sink.description}"
        if new_profile:
            details += f"\nNew Profile: {new_profile.description}"

        notify("Sink Changed", details)
コード例 #4
0
ファイル: core.py プロジェクト: mishurov/applets
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()
コード例 #5
0
ファイル: maxime.py プロジェクト: cohoe/maxime
class PulseAudio:
    """
    PulseAudio connection
    """
    BT_CARD_PREFIX = "bluez_card"
    BT_PROFILE_A2DP = "a2dp_sink"
    BT_PROFILE_HSP = "headset_head_unit"

    def __init__(self, config, bt_device, sp_device, hs_device):
        """
        Constructor for PulseAudio connection.
        :param config: 
        """
        self.config = config
        self.pulse_conn = PulseLib('maxime-manage_connection')
        self.ladspa_device = self._lookup_sink_input_device("LADSPA Stream")
        self.bt_device = bt_device
        self.hs_device = hs_device
        self.sp_device = sp_device

    def activate_wireless(self, conn_event=True):
        """
        Activate the wireless device. If it's a (dis)connect event,
        also mute the speakers so we don't blast audio.
        :param conn_event:
        :return:
        """
        logging.debug("Activating wireless.")

        device_name = self.bt_device.output_device

        # We need to a wait a few seconds for Pulse to catch up
        first_run = True
        while True:
            try:
                target_device = self._lookup_sink_output_device(device_name)
                break
            except Exception as e:
                if "not found" in e.message:
                    if first_run is True:
                        DBusHelper.send_notification(
                            "Routing to %s..." % device_name,
                            DBusHelper.ICON_WIRELESS)
                        first_run = False
                    logging.debug(
                        "Sleeping for 1 second so that Pulse can sort itself out."
                    )
                    time.sleep(1)
                logging.error("Unable to find wireless device.")

        logging.debug("Target device is \"%s\"" % target_device.description)

        # This event check is used to make sure the headphones being
        # (un)intentionally disconnected don't suddenly blast loud noises
        # out of the speakers.
        if conn_event is True:
            logging.debug("This is a connection event. Unmuting wireless.")
            self._unmute(self.ladspa_device)
        self._move_output(self.ladspa_device, target_device,
                          DBusHelper.ICON_WIRELESS)

    def resync_wireless(self):
        """
        Resync a wireless stream.
        :return: 
        """
        # This uses a card identifer rather than a device. No idea if that matters.
        card_dev = self._lookup_card(self.BT_CARD_PREFIX)

        # There is no direct way to resync a stream to the wireless
        # device, but the folks on this here forum have found a
        # way to make it sorta work.
        # https://askubuntu.com/questions/145935/get-rid-of-0-5s-latency-when-playing-audio-over-bluetooth-with-a2dp
        logging.debug("Setting profile of \"%s\" to \"%s\"" %
                      (card_dev.name, self.BT_PROFILE_HSP))
        DBusHelper.send_notification("Resyncing Bluetooth audio stream.",
                                     icon=DBusHelper.ICON_GENERIC)
        self.pulse_conn.card_profile_set(card_dev, self.BT_PROFILE_HSP)
        # We need to let Pulse catch its breath.
        time.sleep(1)
        logging.debug("Setting profile of \"%s\" to \"%s\"" %
                      (card_dev.name, self.BT_PROFILE_A2DP))
        self.pulse_conn.card_profile_set(card_dev, self.BT_PROFILE_A2DP)

        # Switching profiles makes the sinks change, so we need to reroute.
        # @TODO might need to switch conn_even to true if there are mute issues
        self.activate_wireless(conn_event=False)

    def activate_headset(self, conn_event=True):
        """
        Activate the headset device.
        :param conn_event:
        :return:
        """
        logging.debug("Activating headset.")

        out_device_name = self.hs_device.output_device
        in_device_name = self.hs_device.input_device
        try:
            target_output_device = self._lookup_sink_output_device(
                out_device_name)
            target_input_device = self._lookup_source_device(in_device_name)
        except:
            return

        logging.debug("Target output device is \"%s\"" %
                      target_output_device.description)
        self._move_output(self.ladspa_device, target_output_device,
                          DBusHelper.ICON_HEADSET)
        self._set_input(target_input_device)

    def activate_speakers(self, conn_event=True):
        logging.debug("Activating speakers.")

        device_name = self.sp_device.output_device
        target_device = self._lookup_sink_output_device(device_name)
        logging.debug("Target device is \"%s\"" % target_device.description)

        # This event check is used to make sure the headphones being
        # (un)intentionally disconnected don't suddenly blast loud noises
        # out of the speakers.
        if conn_event is True:
            logging.debug("This is a connection event. Muting speakers.")
            self._mute(self.ladspa_device)
        self._move_output(self.ladspa_device, target_device,
                          DBusHelper.ICON_SPEAKERS)

    def _lookup_sink_input_device(self, name):
        """
        Return a Pulse sink input device. These are the items in the "Playback"
        tab in pavucontrol.
        :param name: 
        :return: 
        """
        for device in self.pulse_conn.sink_input_list():
            if device.name == name:
                return device

        logging.error(
            "Sink Input device not found! (Was searching for \"%s\")" % name)

    def _lookup_sink_output_device(self, description):
        """
        Find a Pulse Sink device. These are what show up in the "Output Devices"
        tab in pavucontrol.
        :param prefix: 
        :param description: 
        :return: 
        """
        for device in self.pulse_conn.sink_list():
            if description in device.description:
                return device

        logging.error(
            "Sink Input device not found! (Was searching for \"%s\")" %
            description)
        raise Exception(
            "Sink Input device not found! (Was searching for \"%s\")" %
            description)

    def _lookup_source_device(self, description):
        """
        Find a Pulse source device. These are what show up in the "Input Devices"
        tab in pavucontrol.
        :param prefix: 
        :param description: 
        :return: 
        """
        for device in self.pulse_conn.source_list():
            if device.description == description:
                return device

        logging.error("Source device not found! (Was searching for \"%s\")" %
                      description)
        raise Exception("Source device not found! (Was searching for \"%s\")" %
                        description)

    def _lookup_card(self, name):
        """
        Find a Pulse card. This is the equivalent of pacmd list-cards.
        :param name: The string to search for in the name of the card.
        :return: 
        """
        for device in self.pulse_conn.card_list():
            if name in device.name:
                return device
        logging.error("Card \"%s\" not found!" % name)
        raise Exception("Card \"%s\" not found!" % name)

    def _move_output(self, source, destination, icon):
        """
        Move a Pulse stream
        :param source: Source device that we want to redirect.
        :param destination: Target device that we want to hear from.
        :return: None
        """
        logging.info("Moving stream of \"%s\" to \"%s\"" %
                     (source.name, destination.description))
        self.pulse_conn.sink_input_move(source.index, destination.index)

        text = "Routed %s to %s" % (source.name, destination.description)
        DBusHelper.send_notification(text, icon)

    def _set_input(self, device):
        """
        Set Pulse input device.
        :param device: 
        :return: 
        """
        logging.info("Setting default source device to \"%s\"" %
                     device.description)
        self.pulse_conn.source_default_set(device.name)

    def manage_connection(self, conn_state):
        """
        Decide what to activate based on connection event
        :param conn_state: Boolean of whether the device was connected or not.
        :return: None
        """
        if conn_state is True:
            # Connection
            self.activate_wireless(conn_event=True)
        elif conn_state is False:
            # Disconnection
            self.activate_speakers(conn_event=True)

    def _mute(self, device):
        """
        Mute a sink device
        :param device:
        :return:
        """
        logging.debug("Muting device \"%s\"" % device.name)
        self.pulse_conn.sink_input_mute(device.index, True)

    def _unmute(self, device):
        """
        Unmute a sink input device.
        :param device:
        :return:
        """
        logging.debug("Unmuting device \"%s\"" % device.name)
        self.pulse_conn.sink_input_mute(device.index, False)