Beispiel #1
0
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)
Beispiel #2
0
class SinksMonitoring(QtCore.QThread):

    addSinkInput = QtCore.pyqtSignal(PulseSinkInputInfo)
    removeSinkInput = QtCore.pyqtSignal(PulseSinkInputInfo)
    changeVolumeInput = QtCore.pyqtSignal(PulseSinkInputInfo)

    addSinkOutput = QtCore.pyqtSignal(PulseSourceOutputInfo)
    removeSinkOutput = QtCore.pyqtSignal(PulseSourceOutputInfo)
    changeVolumeOutput = QtCore.pyqtSignal(PulseSourceOutputInfo)

    def __init__(self):
        QtCore.QThread.__init__(self)

        self.pulse = Pulse('volume-mixer-monitor')

        self.__sinksInput = {}
        self.__sinksOutput = {}

    def run(self):
        while True:
            sinksInputNow = self.pulse.sink_input_list()
            removeSinksInput = self.__sinksInput.copy()

            for sinkNow in sinksInputNow:
                id = sinkNow.index

                if id not in self.__sinksInput:
                    self.__sinksInput[id] = sinkNow
                    self.addSinkInput.emit(sinkNow)
                else:
                    if sinkNow.volume.values != self.__sinksInput[
                            id].volume.values:
                        self.__sinksInput[id] = sinkNow
                        self.changeVolumeInput.emit(sinkNow)

                    del removeSinksInput[id]

            for key, sink, in removeSinksInput.items():
                self.removeSinkInput.emit(sink)
                del self.__sinksInput[key]

            sinksOutputNow = self.pulse.source_output_list()
            removeSinksOutput = self.__sinksOutput.copy()

            for sinkNow in sinksOutputNow:
                id = sinkNow.index

                if id not in self.__sinksOutput:
                    self.__sinksOutput[id] = sinkNow
                    self.addSinkOutput.emit(sinkNow)
                else:
                    if sinkNow.volume.values != self.__sinksOutput[
                            id].volume.values:
                        self.__sinksOutput[id] = sinkNow
                        self.changeVolumeOutput.emit(sinkNow)

                    del removeSinksOutput[id]

            for key, sink, in removeSinksOutput.items():
                self.removeSinkOutput.emit(sink)
                del self.__sinksOutput[key]

            self.msleep(10)

    def __del__(self):
        print("Pulse close")
        self.pulse.close()
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()
Beispiel #4
0
class PulseManager:
    """Manage connection to PulseAudio and receive updates."""
    def __init__(self, volctl):
        self._volctl = volctl
        self._pulse_loop_paused = False
        self._pulse = Pulse(client_name=PROGRAM_NAME, connect=False)

        self._poller_thread = None
        self._pulse_lock, self._pulse_hold = threading.Lock(), threading.Lock()
        signal.signal(signal.SIGALRM, self._handle_pulse_events)

        # Stream monitoring
        self._monitor_streams = {}
        self._read_cb_ctypes = c.PA_STREAM_REQUEST_CB_T(self._read_cb)
        self._samplespec = c.PA_SAMPLE_SPEC(format=c.PA_SAMPLE_FLOAT32BE,
                                            rate=25,
                                            channels=1)

        self._connect()

    def close(self):
        """Close the PulseAudio connection and event polling thread."""
        self.stop_peak_monitor()
        self._stop_polling()
        if self._pulse:
            self._pulse.close()

    @contextmanager
    def pulse(self):
        """
        Yield the pulse object, pausing the pulse event loop.
        See https://github.com/mk-fg/python-pulse-control#event-handling-code-threads
        """
        if self._pulse_loop_paused:
            yield self._pulse
        else:
            # Pause PulseAudio event loop
            with self._pulse_hold:
                for _ in range(int(2.0 / 0.05)):
                    # Event loop might not be started yet, so wait
                    self._pulse.event_listen_stop()
                    if self._pulse_lock.acquire(timeout=0.05):
                        break
                else:
                    raise RuntimeError("Could not aquire _pulse_lock!")
                self._pulse_loop_paused = True
                try:
                    yield self._pulse
                finally:
                    self._pulse_lock.release()
                    self._pulse_loop_paused = False

    def _connect(self):
        self._pulse.connect(wait=True)
        print("PulseAudio connected")
        self._start_polling()
        GLib.idle_add(self._volctl.on_connected)

    def _handle_pulse_events(self, *_):
        if self._poller_thread and self._poller_thread.is_alive():
            # Remove transient events and duplicates
            events = OrderedDict()
            while self._poller_thread.events:
                event = self._poller_thread.events.popleft()

                new_tuple = (PulseEventTypeEnum.new, event.facility,
                             event.index)
                if event.t == PulseEventTypeEnum.remove and events.pop(
                        new_tuple, False):
                    change_tuple = (
                        PulseEventTypeEnum.change,
                        event.facility,
                        event.index,
                    )
                    events.pop(change_tuple, None)
                else:
                    events[event.t, event.facility, event.index] = event

            for event in events.values():
                GLib.idle_add(self._handle_event, event)
            self._poller_thread.event_timer_set = False

        # Reconnect on lost connection
        if not self._pulse.connected:
            GLib.idle_add(self._volctl.on_disconnected)
            self._stop_polling()
            self._connect()

    def _start_polling(self):
        self._poller_thread = PulsePoller(self._pulse, self._pulse_lock,
                                          self._pulse_hold, self._handle_event)
        self._poller_thread.start()

    def _stop_polling(self):
        if self._poller_thread and self._poller_thread.is_alive():
            self._poller_thread.quit = True
            self._poller_thread.join(timeout=1.0)
            self._poller_thread = None

    def _handle_event(self, event):
        """Handle PulseAudio event."""
        fac = "sink" if event.facility == "sink" else "sink_input"

        if event.t == PulseEventTypeEnum.change:
            method, obj = None, None

            with self.pulse() as pulse:
                obj_list = getattr(pulse, f"{fac}_list")()
                obj = get_by_attr(obj_list, "index", event.index)

            if obj:
                method = getattr(self._volctl, f"{fac}_update")
                method(event.index, obj.volume.value_flat, obj.mute == 1)

        elif event.t in (PulseEventTypeEnum.new, PulseEventTypeEnum.remove):
            self._volctl.slider_count_changed()

        else:
            print(f"Warning: Unhandled event type for {fac}: {event.t}")

    def _read_cb(self, stream, nbytes, idx):
        data = c.c_void_p()
        nbytes = c.c_int(nbytes)
        idx -= 1
        c.pa.stream_peek(stream, data, c.byref(nbytes))
        try:
            if not data or nbytes.value < 1:
                return
            samples = c.cast(data, c.POINTER(c.c_float))
            val = max(samples[i] for i in range(nbytes.value))
        finally:
            # stream_drop() flushes buffered data (incl. buff=NULL "hole" data)
            # stream.h: "should not be called if the buffer is empty"
            if nbytes:
                c.pa.stream_drop(stream)
        GLib.idle_add(self._volctl.peak_update, idx, min(val, 1.0))

    def start_peak_monitor(self):
        """Start peak monitoring for all sinks and sink inputs."""
        with self.pulse() as pulse:
            for sink in pulse.sink_list():
                stream = self._create_peak_stream(sink.index)
                self._monitor_streams[sink.index] = stream
            for sink_input in pulse.sink_input_list():
                sink_idx = self._pulse.sink_input_info(sink_input.index).sink
                stream = self._create_peak_stream(sink_idx, sink_input.index)
                self._monitor_streams[sink_input.index] = stream

    def stop_peak_monitor(self):
        """Stop peak monitoring for all sinks and sink inputs."""
        with self.pulse():
            for idx, stream in self._monitor_streams.items():
                try:
                    c.pa.stream_disconnect(stream)
                except c.pa.CallError:
                    pass  # Stream was removed
                finally:
                    GLib.idle_add(self._volctl.peak_update, idx, 0.0)
        self._monitor_streams = {}

    def _create_peak_stream(self, sink_idx, sink_input_idx=None):
        # Cannot use `get_peak_sample` from python-pulse-control as it would block GUI.
        proplist = c.pa.proplist_from_string(  # Hide this stream in mixer apps
            "application.id=org.PulseAudio.pavucontrol")
        pa_context = self._pulse._ctx  # pylint: disable=protected-access
        idx = sink_idx if sink_input_idx is None else sink_input_idx
        stream = c.pa.stream_new_with_proplist(pa_context, f"peak {idx}",
                                               c.byref(self._samplespec), None,
                                               proplist)
        c.pa.proplist_free(proplist)
        c.pa.stream_set_read_callback(stream, self._read_cb_ctypes, idx + 1)
        if sink_input_idx is not None:
            # Monitor single sink input
            c.pa.stream_set_monitor_stream(stream, sink_input_idx)
        c.pa.stream_connect_record(
            stream,
            str(sink_idx).encode("utf-8"),
            c.PA_BUFFER_ATTR(fragsize=4, maxlength=2**32 - 1),
            c.PA_STREAM_DONT_MOVE
            | c.PA_STREAM_PEAK_DETECT
            | c.PA_STREAM_ADJUST_LATENCY
            | c.PA_STREAM_DONT_INHIBIT_AUTO_SUSPEND,
        )
        return stream

    # Set/get PulseAudio entities

    def set_main_volume(self, val):
        """Set default sink volume."""
        with self.pulse() as pulse:
            pulse.volume_set_all_chans(self.default_sink, val)

    def toggle_main_mute(self):
        """Toggle default sink mute."""
        with self.pulse():
            self.sink_set_mute(self.default_sink_idx,
                               not self.default_sink.mute)

    def sink_set_mute(self, idx, mute):
        """Set sink mute."""
        with self.pulse() as pulse:
            pulse.sink_mute(idx, mute)

    def sink_input_set_mute(self, idx, mute):
        """Set sink input mute."""
        with self.pulse() as pulse:
            pulse.sink_input_mute(idx, mute)

    @property
    def volume(self):
        """Volume of the default sink."""
        try:
            return self.default_sink.volume.value_flat
        except AttributeError:
            return 0.0

    @property
    def mute(self):
        """Mute state of the default sink."""
        try:
            return self.default_sink.mute == 1
        except AttributeError:
            return False

    @property
    def default_sink(self):
        """Default sink."""
        with self.pulse() as pulse:
            sink_name = pulse.server_info().default_sink_name
            return get_by_attr(pulse.sink_list(), "name", sink_name)

    @property
    def default_sink_idx(self):
        """Default sink index."""
        try:
            return self.default_sink.index
        except AttributeError:
            return None