def list_modules(pulseaudio: pulsectl.Pulse) -> List[pulsectl.PulseModuleInfo]:
    """
    Shortcut for the pactl list modules short command.
    :return: String output from the command.
    """
    listable_module_names = [
        'module-null-sink',
        'module-loopback',
        'module-null-source',
        'module-remap-source',
    ]
    return [
        module for module in pulseaudio.module_list()
        if module.name in listable_module_names
    ]
예제 #2
0
class PA2JACK(object):
    jacksink_channel_map = ['aux{}'.format(i) for i in range(16)]
    default_channel_map = ['front-left', 'front-right', 'rear-left' 'rear-right', 'lfe', 'subwoofer', 'side-left', 'side-right']

    def __init__(self,args):

        # get arguments
        if 'allow_reload' in args:
            self.allow_reload = args.allow_reload
        else: self.allow_reload = False
        self.restart = args.internal_restart
        self.num_channels = args.num_channels
        self.pulse_mon = Pulse('pa2jack-monitor') 
        self.pulse_act = Pulse('pa2jack-actor')

        # Get jack sink
        self.jack_sink = self._get_jack_sink()

        # Reload jack module with correct channels
        self.reload_jack_module(self.num_channels)

        # Unload module-remap-sink
        for m in self.pulse_act.module_list():
            if m.name == "module-remap-sink":
                self.pulse_act.module_unload(m.index) 

        # Make our stereo remapping sinks
        self.remap_sink_modules = set([self._new_remap_sink("remap_sink_{}".format(i),start=i*2) for i in range(math.floor(self.num_channels/2))])

        # Get sink indices
        self.remap_sinks = [s.index for s in self.pulse_act.sink_list() if s.owner_module in self.remap_sink_modules]

        # Listen to sink_input events and set handler
        self.pulse_mon.event_mask_set('sink_input')
        self.pulse_mon.event_callback_set(self._pa_event_handler)

    def _get_jack_sink(self):
        try:
            return next(s for s in self.pulse_act.sink_list() if s.name == 'jack_out')
        except StopIteration:
            logging.warn("Couldn't find jack_out sink.")
            return None

    def _default_channel_map(self,n_channels=2,start=0):
        return ",".join(self.default_channel_map[start:start+n_channels])

    def _jack_channel_map(self,start=0,n_channels=2):
        return ",".join(self.jacksink_channel_map[start:start+n_channels])

    def _new_remap_sink(self, sink_name, start=0,n_channels=2):
        logging.debug("making remap-sink: {} ({})".format(sink_name,n_channels))
        default_ch = self._default_channel_map(n_channels)
        aux_channels = self._jack_channel_map(start,n_channels)
        logging.debug("\naux_ch: {}\ndefault_ch: {}".format(aux_channels,default_ch))
        remap_index = self.pulse_act.module_load("module-remap-sink",
                               "sink_name={} master={} channel_map={} master_channel_map={} remix=no".format(
                                            str(sink_name), "jack_out",
                                            default_ch,
                                            aux_channels))
        logging.debug("Done loading module-remap-sink, ind: {}".format(remap_index))
        return remap_index

    def reload_jack_module(self,channels=2,channel_map=jacksink_channel_map):
        """Unload and reload 'module-jack-sink' with specified number of channels.

        Used to dynamically increase the number of available channels if the initial JACK sink cannot accommodate the number of PA streams.
        Will cause a small stutter in the audio, so only enable if that's okay with your environment."""

        #TODO: Insert check for module-default-device-restore // module-rescue-streams before reloading
        try:
            self.pulse_act.module_unload(self.jack_sink.owner_module)
        except:
            logging.warn("Couldn't unload jack module to restart")
        try:
            self.pulse_act.module_load("module-jack-sink","channels={} channel_map={}".format(channels,",".join(channel_map[:channels])))
            self.jack_sink = self._get_jack_sink()
        except:
            logging.warn("Couldn't load JACK module")
            raise NoJackException()

    def _get_dirty_remap_sinks(self):
        return set(map(lambda si: si.sink, self.pulse_act.sink_input_list()))

    def _handle_new_input(self,pa_event):
        # Load remap source module
        logging.debug("New Input: {}".format(pa_event))
        dirty_sinks = self._get_dirty_remap_sinks()
        logging.debug("dirty_sinks: {}".format(dirty_sinks))
        for si in self.remap_sinks:
            if si not in dirty_sinks:
                logging.debug("Moving input {} to sink {}".format(pa_event.index, si))
                self.pulse_act.sink_input_move(pa_event.index,si)
                return

    def _pa_event_handler(self,pa_event):
        """Monitor and distribute Pulseaudio Events.
        """
        logging.debug("Event received: {}".format(pa_event))
        if pa_event.t == 'new' and pa_event.facility == 'source_output':
            self._handle_new_source(pa_event)
        elif pa_event.t == 'new' and pa_event.facility == 'sink_input':
            self._handle_new_input(pa_event)

    def run(self,reloading=False,log=sys.stdout,restart=False):
        """Hacky process monitor catches all exceptions and restarts process if it fails.

        Really you should use a real process monitor, like supervisord or monit."""
        if restart:
            while True:
                try:
                    self.pulse_mon.event_listen()
                except Exception as err:
                    logging.warn("Exception raised in run loop: ".format(err))
                    continue
        else:
            self.pulse_mon.event_listen()