Esempio n. 1
0
class Volume(StatusItem):
    def __init__(self, **kwargs):
        super().__init__()
        self.pulse = Pulse("deity i3bar statusitem")
        self.muted = True
        self.volume = -1

    def refresh(self, periodic):
        if not periodic:
            sink = self.pulse.get_sink_by_name("@DEFAULT_SINK@")
            muted = bool(sink.mute)
            volume = int(round(sink.volume.value_flat * 100))
            has_changed = muted != self.muted or volume != self.volume
            self.muted = muted
            self.volume = volume
            return has_changed
        return False

    def color(self):
        if self.muted:
            return Color.NEUTRAL
        return Color.POSITIVE

    def full_text(self):
        return "\uf028 " + str(self.volume) + "%"
Esempio n. 2
0
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()
Esempio n. 3
0
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()
# short example how to use integriot and pulsectl to control a pulseaudio volume via mqtt
# author: ulno

# TODO: turn these into arguments
mqtt_server = "homeaut"
sink_name = 'alsa_output.usb-Altec_Lansing_Technologies__Inc._Altec_Lansing_XT2_-_USB_Audio-00.analog-stereo'
location = "kitchen"
device = "volume"

from pulsectl import Pulse  # we need this here: https://pypi.org/project/pulsectl/
from integriot import *
from time import sleep

# pulse mixer setup
pulse = Pulse("pulse_mqtt_volume")
sinkoutput = pulse.get_sink_by_name(sink_name)

# mqtt setup
init(mqtt_server)
prefix(location)
p = publisher(device)


def get_volume():
    # needs to be looked up each time as we don't get a volume else
    sinkinfo = pulse.sink_info(sinkoutput.index)
    return sinkinfo.volume.value_flat


def publish_volume(v):
    global p
Esempio n. 5
0
class Feeder(object):
    def __init__(self, i3, bat, bus, bar, display_title=True):
        self.out = ''
        self.datetime = ''
        self.workspaces = dict()
        self.volume = ''
        self.battery = ''
        self.i3 = i3
        self.bar = bar
        self.display_title = display_title
        focused = i3.get_tree().find_focused()
        self.focusedWinTitle = focused.name if not focused.type == 'workspace' and display_title else ''
        self.currentBindingMode = ''
        self.mode = ''
        self.bat = bat
        self.bus = bus
        self.pulse = Pulse('my-client-name')
        self.outputs = ''
        self.notitle = " %%{R B-}%s%%{F-}" % self.bar.sep_left
        self.title = self.notitle

        self.set_outputs()

    def set_outputs(self):
        mon = [[out.name, out.rect.x, out.rect.y]
               for out in self.i3.get_outputs() if out.active]
        quickSort(mon, 0, len(mon) - 1)
        self.outputs = [el[0] for el in mon]

    def on_battery_event(self, device, props, signature):
        perc = round(self.bat.Get(device, "Percentage"))
        state = self.bat.Get(device, "State")
        timeleft = self.bat.Get(device,
                                "TimeToEmpty" if state == 2 else "TimeToFull")
        self.render_battery(perc, state, timeleft)
        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('bat', file=stderr)

    def on_binding_mode_change(self, caller, e):
        self.currentBindingMode = e.change
        if self.currentBindingMode != "default":
            self.render_binding_mode()
        else:
            self.mode = ''

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('bind', file=stderr)

    def on_workspace_focus(self, caller, e):
        self.focusedWinTitle = e.current.find_focused()
        if not self.focusedWinTitle:
            self.title = self.notitle

        self.on_workspace_event(caller, e)
        #print('wksp2', file=stderr)

    def on_workspace_event(self, caller, e):
        for idx, output in enumerate(self.outputs):
            self.render_workspaces(index=idx, display=output)
            self.display(idx)
        print('')
        #print('wksp1', file=stderr)

    def on_timedate_event(self):
        self.render_datetime()

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('time', file=stderr)

    def on_window_close(self, caller, e):
        focusedWin = self.i3.get_tree().find_focused()
        if not focusedWin.type == 'workspace':
            self.focusedWinTitle = focusedWin.name
            self.render_focused_title()
            try:
                for idx, output in enumerate(self.outputs):
                    self.display(idx)
                print('')
            except:
                fd = open('/tmp/lemonbar', 'a')
                fd.write('error title change\n')
        else:
            self.title = self.notitle
            for idx, output in enumerate(self.outputs):
                self.display(idx)
            print('')

    def on_window_title_change(self, caller, e):
        self.focusedWinTitle = e.container.name
        self.render_focused_title()
        try:
            for idx, output in enumerate(self.outputs):
                self.display(idx)
            print('')
        except:
            fd = open('/tmp/lemonbar', 'a')
            fd.write('error title change\n')

    def on_volume_event(self, ev):
        default_output_sink_name = self.pulse.server_info().default_sink_name
        sink = self.pulse.get_sink_by_name(default_output_sink_name)
        self.render_volume(sink)

        for idx, output in enumerate(self.outputs):
            self.display(idx)
        print('')
        #print('vol', file=stderr)

    def render_workspaces(self, index, display):
        wsp_icon = "%%{F- B%s} %%{T2}%s%%{T1}" % (self.bar.colors['in_bg'],
                                                  icon_wsp)
        wsp_items = ''
        sep = self.bar.sep_left

        for wsp in self.i3.get_workspaces():
            wsp_name = wsp.name
            wsp_action = "%%{A1:i3-msg -q workspace %s:}" % wsp_name
            if wsp.output != display:  #and not wsp.urgent:
                continue
            if wsp.focused:
                wsp_items += " %%{R B%s}%s%s%%{F%s T1} %s%%{A}" % (
                    self.bar.colors['foc_bg'], sep, wsp_action,
                    self.bar.colors['foc_fg'], wsp_name)
            elif wsp.urgent:
                wsp_items += " %%{R B%s}%s%s%%{F- T1} %s" % (
                    self.bar.colors['ur_bg'], sep, wsp_action, wsp_name)
            else:
                if not wsp_items:
                    wsp_items += "  %s%%{T1} %s%%{A}" % (wsp_action, wsp_name)
                else:
                    wsp_items += " %%{R B%s}%s%s%%{F- T1} %s%%{A}" % (
                        self.bar.colors['in_bg'], sep, wsp_action, wsp_name)

        self.workspaces[index] = '%s%s' % (wsp_icon, wsp_items)

    def render_focused_title(self):
        sep = self.bar.sep_left
        self.title = " %%{R B%s}%s%%{F- T2} %s %%{T1}%s %%{R B-}%s%%{F-}" % (
            self.bar.colors['ti_bg'], sep, icon_prog, self.focusedWinTitle,
            sep)

    def render_binding_mode(self):
        self.mode = " %%{R B%s}%s%%{F%s} %%{T1} %s" % (
            self.bar.colors['bd_bg'], self.bar.sep_left,
            self.bar.colors['bd_fg'], self.currentBindingMode)

    def render_datetime(self):
        sep = self.bar.sep_right
        cdate = " %%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % (
            '#A0' + self.bar.colors['gen_bg'], sep, icon_clock,
            strftime("%d-%m-%Y"))
        ctime = " %%{F%s}%s%%{R F-} %s %%{B-}" % (self.bar.colors['foc_bg'],
                                                  sep, strftime("%H:%M"))
        self.datetime = "%s%s" % (cdate, ctime)

    def render_volume(self, sink):
        value = round(sink.volume.value_flat * 100)
        mute = bool(sink.mute)
        volume = str(value) + '%' if not mute else 'MUTE'
        self.volume = "%%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % (
            '#A0' + self.bar.colors['gen_bg'], self.bar.sep_right, icon_vol,
            volume)

    def render_battery(self, val, state, timeleft):
        value = str(val) + "%"
        remaining = " (%s)" % strftime(
            "%H:%M", gmtime(timeleft)) if timeleft > 0 else ''
        fg = '-'
        bg = self.bar.colors['foc_bg']
        if state == 2:
            if val > 50:
                fg = fg_battery_50
            elif val > 25:
                fg = fg_battery_25
            elif val > 10:
                fg = fg_battery_10
            else:
                bg = bg_battery_0
        self.battery = " %%{F%s}%s%%{R F%s} %s%s" % (bg, self.bar.sep_right,
                                                     fg, value, remaining)

    def render_all(self, caller=None, e=None):
        # Render one bar per each output
        if e == None:
            device = "org.freedesktop.UPower.Device"
            perc = round(self.bat.Get(device, "Percentage"))
            state = self.bat.Get(device, "State")
            timeleft = self.bat.Get(
                device, "TimeToEmpty" if state == 2 else "TimeToFull")
            default_output_sink_name = self.pulse.server_info(
            ).default_sink_name
            sink = self.pulse.get_sink_by_name(default_output_sink_name)

            self.render_battery(perc, state, timeleft)
            self.render_datetime()
            self.render_volume(sink)
            if not self.focusedWinTitle == '': self.render_focused_title()

        for idx, output in enumerate(self.outputs):
            self.render_workspaces(index=idx, display=output)
            self.display(idx)
        print('')

    def display(self, idx):
        #self.render_volume(0)
        self.out = "%%{S%d}%%{l}%s%s%s%%{r}%s%s%s" % (
            idx, self.workspaces[idx], self.mode, self.title, self.volume,
            self.battery, self.datetime)
        print(self.out, end='')
Esempio n. 6
0
class Py3status:
    py3: Py3
    blocks = u"_▁▂▃▄▅▆▇█"
    button_down = 5
    button_mute = 1
    button_up = 4
    format = u"{icon} {percentage}%"
    format_muted = u"{icon} {percentage}%"
    is_input = False
    max_volume = 100
    thresholds = [(0, "good"), (75, "degraded"), (100, "bad")]
    volume_delta = 5

    def __init__(self,
                 sink_name: Optional[str] = None,
                 volume_boost: bool = False):
        """

        :param sink_name:  Sink name to use. Empty uses default sink
        :param volume_boost: Whether to allow setting volume above 1.0 - uses software boost
        """
        self._sink_name = sink_name
        self._sink_info: Optional[PulseSinkInfo]
        self._volume_boost = volume_boost
        self._pulse_connector = Pulse('py3status-pulse-connector',
                                      threading_lock=True)
        self._pulse_connector_lock = threading.Lock()
        self._volume: Optional[Volume] = None
        self._backend_thread = threading.Thread

    def _get_volume_from_backend(self):
        """Get a new sink on every call.

        The sink is not updated when the backed values change.
        Returned volume is the maximum of all available channels.
        """
        sink_name = self._pulse_connector.server_info().default_sink_name
        self._sink_info = self._pulse_connector.get_sink_by_name(sink_name)
        pulse_volume = Volume.from_sink_info(self._sink_info)
        logger.debug(pulse_volume)
        if self._volume != pulse_volume:
            self._volume = pulse_volume
            self.py3.update()

    def _callback(self, ev):
        if ev.t == PulseEventTypeEnum.change and \
                (ev.facility == PulseEventFacilityEnum.server or
                 ev.facility == PulseEventFacilityEnum.sink and ev.index == self._sink_info.index):
            raise PulseLoopStop

    def _pulse_reader(self):
        while True:
            try:
                self._pulse_connector.event_listen()
                self._get_volume_from_backend()
            except PulseDisconnected:
                logger.debug("Pulse disconnected. Stopping reader.")
                break

    def post_config_hook(self):
        self._pulse_connector.connect()
        self._get_volume_from_backend()
        self._pulse_connector.event_mask_set(PulseEventMaskEnum.server,
                                             PulseEventMaskEnum.sink)
        self._pulse_connector.event_callback_set(self._callback)
        self._backend_thread = threading.Thread(
            name="pulse_backend", target=self._pulse_reader).start()

    def kill(self):
        logger.info("Shutting down")
        self._pulse_connector.disconnect()

    def _color_for_output(self) -> str:
        if self._volume is None:
            return self.py3.COLOR_BAD
        if self._volume.mute:
            return self.py3.COLOR_MUTED or self.py3.COLOR_BAD
        return self.py3.threshold_get_color(self._volume.level)

    def _icon_for_output(self) -> str:
        return self.blocks[min(
            len(self.blocks) - 1,
            int(math.ceil(self._volume.level / 100 * (len(self.blocks) - 1))),
        )]

    def _format_output(self) -> Union[str, Composite]:
        return self.py3.safe_format(format_string=self.format_muted
                                    if self._volume.mute else self.format,
                                    param_dict={
                                        "icon": self._icon_for_output(),
                                        "percentage": self._volume.level
                                    })

    def volume_status(self):
        response = {
            "cached_until": self.py3.CACHE_FOREVER,
            "color": self._color_for_output(),
            "full_text": self._format_output()
        }
        return response
Esempio n. 7
0
import sys
import time
from pulsectl import Pulse

pulse = Pulse()
arg = "".join(sys.argv[1:]).strip()
if arg == "":
    exit()

sink = int(arg)
assert isinstance(sink, int)
sink_obj = pulse.get_sink_by_name(arg)

for sink_in in pulse.sink_input_list():
    pulse.sink_input_move(sink_in.index, sink)

time.sleep(1)
pulse.sink_default_set(sink_obj)
Esempio n. 8
0
class pulseaudiostuff():
    def __init__(self):
        logging.debug("Initing %s", self.__class__.__name__)
        self.pulse = Pulse()
        self.barecmd = connect_to_cli()

        default_sink_name = self.pulse.server_info().default_sink_name
        self.default_sink_info = self.pulse.get_sink_by_name(default_sink_name)

        out_sink_name = "game-out"
        # music + mic
        self.out_sink_module_id = self.pulse.module_load(
            "module-null-sink", 'sink_name=' + out_sink_name)
        self.out_sink_info = self.pulse.get_sink_by_name(out_sink_name)

        MP_sink_name = "Media-player"
        # that is our main sink. send your media here
        # everything that comes in is being copied to game sink and default sink
        self.MP_sink_module_id = self.pulse.module_load(
            "module-combine-sink", 'sink_name=' + MP_sink_name + ' slaves=' +
            str(self.out_sink_info.index) + ',' +
            str(self.default_sink_info.index))
        self.MP_sink_info = self.pulse.get_sink_by_name(MP_sink_name)

        # Get stream media -> speakers
        # TODO: this is also gay but it is somehow possible to retreve all inputs for sink. (sink_input_list(sinkIndex))
        for stream in self.pulse.sink_input_list():
            if stream.owner_module == self.MP_sink_module_id:
                if stream.sink == self.default_sink_info.index:
                    self.sound2speakers = stream
                elif stream.sink == self.out_sink_info.index:
                    self.sound2game = stream

        # send mic stream to game sink. (btw rip 20 ms)
        self.loopback_module_id = self.pulse.module_load(
            "module-loopback", 'sink=' + str(self.out_sink_info.index) +
            ' latency_msec=20 source_dont_move=true sink_dont_move=true')

        # Get stream mic -> game
        # TODO: this is also gay but it is somehow possible to retreve all inputs for sink. (sink_input_list(sinkIndex))
        for stream in self.pulse.sink_input_list():
            if stream.sink == self.out_sink_info.index and stream.owner_module == self.loopback_module_id:
                self.mic2game = stream

        # TODO: combine sink sets volume to earrape because reasons?
        hell = self.sound2speakers.volume
        hell.value_flat = 0.5
        self.pulse.volume_set(self.sound2speakers, hell)
        hell.value_flat = 1.0
        self.pulse.volume_set(self.sound2game, hell)

        # TODO: change names of sinks.
        # https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/issues/615
        # self.barecmd.write(
        #     'update-sink-proplist ' + str(self.out_sink_info.index) + ' device.description="Game media player out"')
        # self.barecmd.write(
        #     'update-sink-proplist ' + str(self.MP_sink_info.index) + ' device.description="Game media player sink"')

        self.pulse.sink_default_set(self.default_sink_info)

        logging.debug("%s class loaded", self.__class__.__name__)
        logging.info("out sink module id: %d", self.out_sink_module_id)
        logging.info("MP sink module id: %d", self.MP_sink_module_id)
        logging.info("loopback module id: %d", self.loopback_module_id)

    def __del__(self):
        self.pulse.module_unload(self.loopback_module_id)
        self.pulse.module_unload(self.out_sink_module_id)
        self.pulse.module_unload(self.MP_sink_module_id)
        logging.debug("%s class unloaded", self.__class__.__name__)

    def printstuff(self):
        print('csgo launch options: "pacmd set-default-source ' +
              self.out_sink_info.name + '.monitor"')
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)