class Volume(StatusItem): def __init__(self, **kwargs): super().__init__() self.pulse = Pulse("deity i3bar statusitem") self.muted = True self.volume = -1 def refresh(self, periodic): if not periodic: sink = self.pulse.get_sink_by_name("@DEFAULT_SINK@") muted = bool(sink.mute) volume = int(round(sink.volume.value_flat * 100)) has_changed = muted != self.muted or volume != self.volume self.muted = muted self.volume = volume return has_changed return False def color(self): if self.muted: return Color.NEUTRAL return Color.POSITIVE def full_text(self): return "\uf028 " + str(self.volume) + "%"
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()
# short example how to use integriot and pulsectl to control a pulseaudio volume via mqtt # author: ulno # TODO: turn these into arguments mqtt_server = "homeaut" sink_name = 'alsa_output.usb-Altec_Lansing_Technologies__Inc._Altec_Lansing_XT2_-_USB_Audio-00.analog-stereo' location = "kitchen" device = "volume" from pulsectl import Pulse # we need this here: https://pypi.org/project/pulsectl/ from integriot import * from time import sleep # pulse mixer setup pulse = Pulse("pulse_mqtt_volume") sinkoutput = pulse.get_sink_by_name(sink_name) # mqtt setup init(mqtt_server) prefix(location) p = publisher(device) def get_volume(): # needs to be looked up each time as we don't get a volume else sinkinfo = pulse.sink_info(sinkoutput.index) return sinkinfo.volume.value_flat def publish_volume(v): global p
class Feeder(object): def __init__(self, i3, bat, bus, bar, display_title=True): self.out = '' self.datetime = '' self.workspaces = dict() self.volume = '' self.battery = '' self.i3 = i3 self.bar = bar self.display_title = display_title focused = i3.get_tree().find_focused() self.focusedWinTitle = focused.name if not focused.type == 'workspace' and display_title else '' self.currentBindingMode = '' self.mode = '' self.bat = bat self.bus = bus self.pulse = Pulse('my-client-name') self.outputs = '' self.notitle = " %%{R B-}%s%%{F-}" % self.bar.sep_left self.title = self.notitle self.set_outputs() def set_outputs(self): mon = [[out.name, out.rect.x, out.rect.y] for out in self.i3.get_outputs() if out.active] quickSort(mon, 0, len(mon) - 1) self.outputs = [el[0] for el in mon] def on_battery_event(self, device, props, signature): perc = round(self.bat.Get(device, "Percentage")) state = self.bat.Get(device, "State") timeleft = self.bat.Get(device, "TimeToEmpty" if state == 2 else "TimeToFull") self.render_battery(perc, state, timeleft) for idx, output in enumerate(self.outputs): self.display(idx) print('') #print('bat', file=stderr) def on_binding_mode_change(self, caller, e): self.currentBindingMode = e.change if self.currentBindingMode != "default": self.render_binding_mode() else: self.mode = '' for idx, output in enumerate(self.outputs): self.display(idx) print('') #print('bind', file=stderr) def on_workspace_focus(self, caller, e): self.focusedWinTitle = e.current.find_focused() if not self.focusedWinTitle: self.title = self.notitle self.on_workspace_event(caller, e) #print('wksp2', file=stderr) def on_workspace_event(self, caller, e): for idx, output in enumerate(self.outputs): self.render_workspaces(index=idx, display=output) self.display(idx) print('') #print('wksp1', file=stderr) def on_timedate_event(self): self.render_datetime() for idx, output in enumerate(self.outputs): self.display(idx) print('') #print('time', file=stderr) def on_window_close(self, caller, e): focusedWin = self.i3.get_tree().find_focused() if not focusedWin.type == 'workspace': self.focusedWinTitle = focusedWin.name self.render_focused_title() try: for idx, output in enumerate(self.outputs): self.display(idx) print('') except: fd = open('/tmp/lemonbar', 'a') fd.write('error title change\n') else: self.title = self.notitle for idx, output in enumerate(self.outputs): self.display(idx) print('') def on_window_title_change(self, caller, e): self.focusedWinTitle = e.container.name self.render_focused_title() try: for idx, output in enumerate(self.outputs): self.display(idx) print('') except: fd = open('/tmp/lemonbar', 'a') fd.write('error title change\n') def on_volume_event(self, ev): default_output_sink_name = self.pulse.server_info().default_sink_name sink = self.pulse.get_sink_by_name(default_output_sink_name) self.render_volume(sink) for idx, output in enumerate(self.outputs): self.display(idx) print('') #print('vol', file=stderr) def render_workspaces(self, index, display): wsp_icon = "%%{F- B%s} %%{T2}%s%%{T1}" % (self.bar.colors['in_bg'], icon_wsp) wsp_items = '' sep = self.bar.sep_left for wsp in self.i3.get_workspaces(): wsp_name = wsp.name wsp_action = "%%{A1:i3-msg -q workspace %s:}" % wsp_name if wsp.output != display: #and not wsp.urgent: continue if wsp.focused: wsp_items += " %%{R B%s}%s%s%%{F%s T1} %s%%{A}" % ( self.bar.colors['foc_bg'], sep, wsp_action, self.bar.colors['foc_fg'], wsp_name) elif wsp.urgent: wsp_items += " %%{R B%s}%s%s%%{F- T1} %s" % ( self.bar.colors['ur_bg'], sep, wsp_action, wsp_name) else: if not wsp_items: wsp_items += " %s%%{T1} %s%%{A}" % (wsp_action, wsp_name) else: wsp_items += " %%{R B%s}%s%s%%{F- T1} %s%%{A}" % ( self.bar.colors['in_bg'], sep, wsp_action, wsp_name) self.workspaces[index] = '%s%s' % (wsp_icon, wsp_items) def render_focused_title(self): sep = self.bar.sep_left self.title = " %%{R B%s}%s%%{F- T2} %s %%{T1}%s %%{R B-}%s%%{F-}" % ( self.bar.colors['ti_bg'], sep, icon_prog, self.focusedWinTitle, sep) def render_binding_mode(self): self.mode = " %%{R B%s}%s%%{F%s} %%{T1} %s" % ( self.bar.colors['bd_bg'], self.bar.sep_left, self.bar.colors['bd_fg'], self.currentBindingMode) def render_datetime(self): sep = self.bar.sep_right cdate = " %%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % ( '#A0' + self.bar.colors['gen_bg'], sep, icon_clock, strftime("%d-%m-%Y")) ctime = " %%{F%s}%s%%{R F-} %s %%{B-}" % (self.bar.colors['foc_bg'], sep, strftime("%H:%M")) self.datetime = "%s%s" % (cdate, ctime) def render_volume(self, sink): value = round(sink.volume.value_flat * 100) mute = bool(sink.mute) volume = str(value) + '%' if not mute else 'MUTE' self.volume = "%%{F%s}%s%%{R F-} %%{T2}%s%%{T1} %s" % ( '#A0' + self.bar.colors['gen_bg'], self.bar.sep_right, icon_vol, volume) def render_battery(self, val, state, timeleft): value = str(val) + "%" remaining = " (%s)" % strftime( "%H:%M", gmtime(timeleft)) if timeleft > 0 else '' fg = '-' bg = self.bar.colors['foc_bg'] if state == 2: if val > 50: fg = fg_battery_50 elif val > 25: fg = fg_battery_25 elif val > 10: fg = fg_battery_10 else: bg = bg_battery_0 self.battery = " %%{F%s}%s%%{R F%s} %s%s" % (bg, self.bar.sep_right, fg, value, remaining) def render_all(self, caller=None, e=None): # Render one bar per each output if e == None: device = "org.freedesktop.UPower.Device" perc = round(self.bat.Get(device, "Percentage")) state = self.bat.Get(device, "State") timeleft = self.bat.Get( device, "TimeToEmpty" if state == 2 else "TimeToFull") default_output_sink_name = self.pulse.server_info( ).default_sink_name sink = self.pulse.get_sink_by_name(default_output_sink_name) self.render_battery(perc, state, timeleft) self.render_datetime() self.render_volume(sink) if not self.focusedWinTitle == '': self.render_focused_title() for idx, output in enumerate(self.outputs): self.render_workspaces(index=idx, display=output) self.display(idx) print('') def display(self, idx): #self.render_volume(0) self.out = "%%{S%d}%%{l}%s%s%s%%{r}%s%s%s" % ( idx, self.workspaces[idx], self.mode, self.title, self.volume, self.battery, self.datetime) print(self.out, end='')
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
import sys import time from pulsectl import Pulse pulse = Pulse() arg = "".join(sys.argv[1:]).strip() if arg == "": exit() sink = int(arg) assert isinstance(sink, int) sink_obj = pulse.get_sink_by_name(arg) for sink_in in pulse.sink_input_list(): pulse.sink_input_move(sink_in.index, sink) time.sleep(1) pulse.sink_default_set(sink_obj)
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"')
def main(args): pulse = Pulse("pulse-audio-cycle") current_sink = pulse.get_sink_by_name( pulse.server_info().default_sink_name) current_profile = pulse.card_info(current_sink.card).profile_active # card -> holds all profiles and sets the active one # sink uses a card+profile combination and is names accordingly matching_cards_with_profiles = [] card_pattern = re.compile(args.card) logging.debug(f"card_pattern: {card_pattern}") # Get a list of all matching cards for card in pulse.card_list(): # Prepare to also match against sink description sink_for_current_card = None if args.use_sink_description: sink_for_current_card = sink_for_card(card, pulse) card_pattern_matched = False if re.search(card_pattern, card.name): card_pattern_matched = True logging.info(f"Card matched: {card.name}") if sink_for_current_card: logging.info( f"-> Sink Description: {sink_for_current_card.description}" ) elif args.use_sink_description: if sink_for_current_card: if re.search(card_pattern, sink_for_current_card.description): card_pattern_matched = True logging.info(f"Card matched: {card.name}") logging.info( f"-> Sink Description: {sink_for_current_card.description}" ) logging.info("-> matched via Sink Description") # Ignore cards that are not wanted by the user given pattern if not card_pattern_matched: continue matched_profiles = [] # Check if we need filter for certain profiles or leave as is for profile in card.profile_list: # skip unavailable profiles (unless wanted) if not profile.available and not args.with_unavailable: continue # Check every given profile for cp_card_pattern, cp_profile_pattern in args.profile: cp_card_pattern_matched = False if re.search(cp_card_pattern, card.name): cp_card_pattern_matched = True elif args.use_sink_description: if sink_for_current_card: if re.search(cp_card_pattern, sink_for_current_card.description): cp_card_pattern_matched = True # This cp_profile_pattern does not apply to this card if not cp_card_pattern_matched: continue if re.search(cp_profile_pattern, profile.name): logging.info(f" Profile matched: {profile.name} ") matched_profiles.append(profile) if not matched_profiles: logging.info(" No Profile matched – Keeping profile.") # put infos into list matching_cards_with_profiles.append((card, matched_profiles)) # separator betweem cards logging.info("") new_card, new_profile = new_card_and_profile(matching_cards_with_profiles, pulse) # change profile if necessary if new_profile: if args.verbose: logging.info(f"New Profile: {new_profile.description}") if not args.dry: pulse.card_profile_set(new_card, new_profile) else: if args.verbose: logging.info("NO new Profile.") # change sink (has to be done always because card profile also changes sink) new_sink = sink_for_card(new_card, pulse) if args.verbose: logging.info(f"New Card: {new_card.name}") if args.use_sink_description: logging.info(f"-> New Sink: {new_sink.description} ") if not args.dry: pulse.sink_default_set(new_sink) # move all input sinks (apps/X clients) to new output sink for input_sink in pulse.sink_input_list(): if args.verbose: logging.info( f" -> Switching {input_sink.proplist['application.name']}") if not args.dry: pulse.sink_input_move(input_sink.index, new_sink.index) # Show notification if args.notify: details = f"New Sink: {new_sink.description}" if new_profile: details += f"\nNew Profile: {new_profile.description}" notify("Sink Changed", details)