Beispiel #1
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()
Beispiel #2
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"')