class PulseNerve(): quit = False def __init__(self, cm): self.pulse = Pulse() self.cm = cm self.pulse.event_mask_set('all') self.pulse.event_callback_set(self.state_change) self.pulse.event_listen(timeout=10) def run(self): while not self.quit: time.sleep(0.1) def state_change(self, ev): #psil = self.pulse.sink_input_list() #props = [i.proplist for i in psil] print(ev.index, dir(ev.facility), ev.t)
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()
if event.t == 'new': channels.append(channel(len(channels), event.index)) else: chan = get_channel_by_index(event.index) if event.t == 'change': chan.change_event() elif event.t == 'remove': del channels[chan.channel_id] for key, chan in enumerate(channels): chan.set_id(key) pulse_events.task_done() midi_input = mido.open_input("nanoKONTROL2 36:0") midi_output = mido.open_output("nanoKONTROL2 36:0") pulse.event_mask_set('all') pulse.event_callback_set(handle_pulse_event) clear_leds() # Set up initial channels for chan_id, sink in enumerate(pulse.sink_input_list()): channels.append(channel(chan_id, sink.index)) while True: for msg in midi_input.iter_pending(): if msg.type == 'control_change': handle_cc(msg.control, msg.value) pulse.event_listen(0.001) process_pulse_events()
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()
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()
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