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()
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"')