pax_global_header00006660000000000000000000000064132152205530014510gustar00rootroot0000000000000052 comment=f50c96cbf0df476752416892caa449ed1aae0152 pulsemixer-1.4.0/000077500000000000000000000000001321522055300137075ustar00rootroot00000000000000pulsemixer-1.4.0/LICENSE000066400000000000000000000020601321522055300147120ustar00rootroot00000000000000MIT License Copyright (c) 2017 George Filipkin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pulsemixer-1.4.0/README.md000066400000000000000000000070741321522055300151760ustar00rootroot00000000000000# pulsemixer cli and curses mixer for pulseaudio ### Requirements: - `Python` >= 3 - `Pulseaudio` >= ? ### Installation: `pip3 install git+https://github.com/GeorgeFilipkin/pulsemixer.git` Or just download pulsemixer manually, do `chmod +x ./pulsemixer` and put it anywhere you want. ### CLI usage: ``` Usage of pulsemixer: -h, --help show this help message and exit -v, --version print version -l, --list list everything --list-sources list sources --list-sinks list sinks --id ID specify ID. If no ID specified - default sink is used --set-volume n set volume for ID --set-volume-all n:n set volume for ID (for every channel) --change-volume +-n change volume for ID --get-mute get mute for ID --toggle-mute toggle mute for ID --get-volume get volume for ID --mute mute ID --unmute unmute ID --server choose the server to connect to --color n 0 no color, 1 color currently selected, 2 full-color (default) --no-mouse disable mouse support ``` It is possible to repeat arguments: ``` pulsemixer --get-volume --change-volume +5 --get-volume 65 65 70 70 ``` **Note on `--id`:** `--id` must be specified before the set/get/mute command, i.e. `pulsemixer --id 470 --get-volume`. If `--id` isn't specified of specified after the command, then that command will use default sink. It is not the most common cli behavior (and may be changed in the future), but it was done to avoid consecutive calls to pulsemixer, to chain commands with a single call. For example you could do this: ``` pulsemixer --id 470 --get-volume --id 2 --get-volume --change-volume +5 --get-volume 90 90 100 100 105 105 ``` ### Interactive mode: Interactive mode is used when no arguments were given (except `--color` and `--server`) ![Image of 1](../img/1.png?raw=true) ![Image of 2](../img/2.png?raw=true) Interactive controls: ``` h/j/k/l, arrows navigation, volume change H/L, Shift+Left/Shift+Right change volume by 10 1/2/3/4/5/6/7/8/9/0 set volume to 10%-100% m mute/unmute Space lock/unlock channels together Enter context menu F1/F2/F3 change modes Tab go to next mode Mouse left click select device or mode Mouse wheel volume change q/Esc/^C quit ``` Via context menu it is possible to `set-default-sink`, `set-default-source`, `move-sink-input`, `move-source-output`, `suspend-sink`, `suspend-source`, `set-sink-port`, `set-source-port`, `kill-client`, `kill-sink-input`, `kill-source-output`, `set-card-profile`. See `man pactl` for details on these features. ### Customizing: The volume bar's appearance can be changed with the environment variable PULSEMIXER_BAR_STYLE. The bar characters are defined as: ``` PULSEMIXER_BAR_STYLE="xxxxxxxxxxx" ||||||||||| top left corner -+|||||||||| left side (mono) --+||||||||| top right corner ---+|||||||| right side (mono) ----+||||||| bottom left corner -----+|||||| bottom right corner ------+||||| bar segment (on) -------+|||| bar segment (off) --------+||| channel (deselected) ---------+|| channel (selected) ----------+| channel (linked) -----------+ ``` To set the bar style in (e.g.) zsh: ``` export PULSEMIXER_BAR_STYLE="┌╶┐╴└┘♥ ◆┆" ``` ## License This project is licensed under the terms of the MIT license pulsemixer-1.4.0/pulsemixer000066400000000000000000002221261321522055300160340ustar00rootroot00000000000000#!/usr/bin/env python3 '''Usage of pulsemixer: -h, --help show this help message and exit -v, --version print version -l, --list list everything --list-sources list sources --list-sinks list sinks --id ID specify ID. If no ID specified - default sink is used --set-volume n set volume for ID --set-volume-all n:n set volume for ID (for every channel) --change-volume +-n change volume for ID --get-mute get mute for ID --toggle-mute toggle mute for ID --get-volume get volume for ID --mute mute ID --unmute unmute ID --server choose the server to connect to --color n 0 no color, 1 color currently selected, 2 full-color (default) --no-mouse disable mouse support''' VERSION = '1.4.0' from ctypes import * from time import sleep import operator, functools import sys import curses import re import getopt import signal import traceback from os import environ, getenv from pprint import pprint ######################################################################################### # v bindings try: p = CDLL("libpulse.so.0") except Exception as err: sys.exit(err) PA_VOLUME_NORM = 65536 PA_CHANNELS_MAX = 32 PA_USEC_T = c_uint64 bar_style = getenv('PULSEMIXER_BAR_STYLE', '┌╶┐╴└┘▮- ──').ljust(11, '?') BAR_TOP_LEFT = bar_style[0] BAR_LEFT_MONO = bar_style[1] BAR_TOP_RIGHT = bar_style[2] BAR_RIGHT_MONO = bar_style[3] BAR_BOTTOM_LEFT = bar_style[4] BAR_BOTTOM_RIGHT = bar_style[5] BAR_ON = bar_style[6] BAR_OFF = bar_style[7] ARROW = bar_style[8] ARROW_FOCUSED = bar_style[9] ARROW_LOCKED = bar_style[10] class PA_MAINLOOP(Structure): pass class PA_MAINLOOP_API(Structure): pass class PA_CONTEXT(Structure): pass class PA_OPERATION(Structure): pass class PA_PROPLIST(Structure): pass class PA_SAMPLE_SPEC(Structure): _fields_ = [ ("format", c_int), ("rate", c_uint32), ("channels", c_uint32) ] class PA_CHANNEL_MAP(Structure): _fields_ = [ ("channels", c_uint8), ("map", c_int * PA_CHANNELS_MAX) ] class PA_CVOLUME(Structure): _fields_ = [ ("channels", c_uint8), ("values", c_uint32 * PA_CHANNELS_MAX) ] class PA_PORT_INFO(Structure): _fields_ = [ ('name', c_char_p), ('description', c_char_p), ('priority', c_uint32), ] class PA_SINK_INPUT_INFO(Structure): _fields_ = [ ("index", c_uint32), ("name", c_char_p), ("owner_module", c_uint32), ("client", c_uint32), ("sink", c_uint32), ("sample_spec", PA_SAMPLE_SPEC), ("channel_map", PA_CHANNEL_MAP), ("volume", PA_CVOLUME), ("buffer_usec", PA_USEC_T), ("sink_usec", PA_USEC_T), ("resample_method", c_char_p), ("driver", c_char_p), ("mute", c_int), ("proplist", POINTER(PA_PROPLIST)) ] class PA_SINK_INFO(Structure): _fields_ = [ ("name", c_char_p), ("index", c_uint32), ("description", c_char_p), ("sample_spec", PA_SAMPLE_SPEC), ("channel_map", PA_CHANNEL_MAP), ("owner_module", c_uint32), ("volume", PA_CVOLUME), ("mute", c_int), ("monitor_source", c_uint32), ("monitor_source_name", c_char_p), ("latency", PA_USEC_T), ("driver", c_char_p), ("flags", c_int), ("proplist", POINTER(PA_PROPLIST)), ("configured_latency", PA_USEC_T), ('base_volume', c_int), ('state', c_int), ('n_volume_steps', c_int), ('card', c_uint32), ('n_ports', c_uint32), ('ports', POINTER(POINTER(PA_PORT_INFO))), ('active_port', POINTER(PA_PORT_INFO)) ] class PA_SOURCE_OUTPUT_INFO(Structure): _fields_ = [ ("index", c_uint32), ("name", c_char_p), ("owner_module", c_uint32), ("client", c_uint32), ("source", c_uint32), ("sample_spec", PA_SAMPLE_SPEC), ("channel_map", PA_CHANNEL_MAP), ("buffer_usec", PA_USEC_T), ("source_usec", PA_USEC_T), ("resample_method", c_char_p), ("driver", c_char_p), ("proplist", POINTER(PA_PROPLIST)), ("corked", c_int), ("volume", PA_CVOLUME), ("mute", c_int), ] class PA_SOURCE_INFO(Structure): _fields_ = [ ("name", c_char_p), ("index", c_uint32), ("description", c_char_p), ("sample_spec", PA_SAMPLE_SPEC), ("channel_map", PA_CHANNEL_MAP), ("owner_module", c_uint32), ("volume", PA_CVOLUME), ("mute", c_int), ("monitor_of_sink", c_uint32), ("monitor_of_sink_name", c_char_p), ("latency", PA_USEC_T), ("driver", c_char_p), ("flags", c_int), ("proplist", POINTER(PA_PROPLIST)), ("configured_latency", PA_USEC_T), ('base_volume', c_int), ('state', c_int), ('n_volume_steps', c_int), ('card', c_uint32), ('n_ports', c_uint32), ('ports', POINTER(POINTER(PA_PORT_INFO))), ('active_port', POINTER(PA_PORT_INFO)) ] class PA_CLIENT_INFO(Structure): _fields_ = [ ("index", c_uint32), ("name", c_char_p), ("owner_module", c_uint32), ("driver", c_char_p) ] class PA_CARD_PROFILE_INFO(Structure): _fields_ = [ ('name', c_char_p), ('description', c_char_p), ('n_sinks', c_uint32), ('n_sources', c_uint32), ('priority', c_uint32), ] class PA_CARD_INFO(Structure): _fields_ = [ ('index', c_uint32), ('name', c_char_p), ('owner_module', c_uint32), ('driver', c_char_p), ('n_profiles', c_uint32), ('profiles', POINTER(PA_CARD_PROFILE_INFO)), ('active_profile', POINTER(PA_CARD_PROFILE_INFO)), ('proplist', POINTER(PA_PROPLIST)), ] class PA_SERVER_INFO(Structure): _fields_ = [ ('user_name', c_char_p), ('host_name', c_char_p), ('server_version', c_char_p), ('server_name', c_char_p), ('sample_spec', PA_SAMPLE_SPEC), ('default_sink_name', c_char_p), ('default_source_name', c_char_p), ] PA_STATE_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), c_void_p) PA_SIGNAL_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_MAINLOOP_API), POINTER(c_int), c_int, c_void_p) PA_CLIENT_INFO_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), POINTER(PA_CLIENT_INFO), c_int, c_void_p) PA_SINK_INPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INPUT_INFO), c_int, c_void_p) PA_SINK_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INFO), c_int, c_void_p) PA_SOURCE_OUTPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_OUTPUT_INFO), c_int, c_void_p) PA_SOURCE_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_INFO), c_int, c_void_p) PA_CONTEXT_SUCCESS_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), c_int, c_void_p) PA_CARD_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_CARD_INFO), c_int, c_void_p) PA_SERVER_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_SERVER_INFO), c_void_p) PA_CONTEXT_READY = 4 PA_CONTEXT_FAILED = 5 PA_CONTEXT_TERMINATED = 6 pa_mainloop_new = p.pa_mainloop_new pa_mainloop_new.restype = POINTER(PA_MAINLOOP) pa_mainloop_new.argtypes = [] pa_mainloop_get_api = p.pa_mainloop_get_api pa_mainloop_get_api.restype = POINTER(PA_MAINLOOP_API) pa_mainloop_get_api.argtypes = [POINTER(PA_MAINLOOP)] pa_mainloop_run = p.pa_mainloop_run pa_mainloop_run.restype = c_int pa_mainloop_run.argtypes = [POINTER(PA_MAINLOOP), POINTER(c_int)] pa_mainloop_iterate = p.pa_mainloop_iterate pa_mainloop_iterate.restype = c_int pa_mainloop_iterate.argtypes = [POINTER(PA_MAINLOOP), c_int, POINTER(c_int)] pa_mainloop_free = p.pa_mainloop_free pa_mainloop_free.restype = c_int pa_mainloop_free.argtypes = [POINTER(PA_MAINLOOP)] pa_signal_init = p.pa_signal_init pa_signal_init.restype = c_int pa_signal_init.argtypes = [POINTER(PA_MAINLOOP_API)] pa_signal_new = p.pa_signal_new pa_signal_new.restype = None pa_signal_new.argtypes = [c_int, PA_SIGNAL_CB_T, POINTER(c_int)] pa_context_errno = p.pa_context_errno pa_context_errno.restype = c_int pa_context_errno.argtypes = [POINTER(PA_CONTEXT)] pa_context_new = p.pa_context_new pa_context_new.restype = POINTER(PA_CONTEXT) pa_context_new.argtypes = [POINTER(PA_MAINLOOP_API), c_char_p] pa_context_unref = p.pa_context_unref pa_context_unref.restype = None pa_context_unref.argtypes = [POINTER(PA_CONTEXT)] pa_context_set_state_callback = p.pa_context_set_state_callback pa_context_set_state_callback.restype = None pa_context_set_state_callback.argtypes = [ POINTER(PA_CONTEXT), PA_STATE_CB_T, c_void_p ] pa_context_connect = p.pa_context_connect pa_context_connect.restype = c_int pa_context_connect.argtypes = [ POINTER(PA_CONTEXT), c_char_p, c_int, POINTER(c_int) ] pa_context_get_state = p.pa_context_get_state pa_context_get_state.restype = c_int pa_context_get_state.argtypes = [POINTER(PA_CONTEXT)] pa_context_disconnect = p.pa_context_disconnect pa_context_disconnect.restype = c_int pa_context_disconnect.argtypes = [POINTER(PA_CONTEXT)] pa_proplist_gets = p.pa_proplist_gets pa_proplist_gets.restype = c_char_p pa_proplist_gets.argtypes = [POINTER(PA_PROPLIST), c_char_p] pa_context_get_sink_input_info_list = p.pa_context_get_sink_input_info_list pa_context_get_sink_input_info_list.restype = POINTER(c_int) pa_context_get_sink_input_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_SINK_INPUT_INFO_CB_T, c_void_p ] pa_context_get_sink_info_list = p.pa_context_get_sink_info_list pa_context_get_sink_info_list.restype = POINTER(c_int) pa_context_get_sink_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_SINK_INFO_CB_T, c_void_p ] pa_context_set_sink_mute_by_index = p.pa_context_set_sink_mute_by_index pa_context_set_sink_mute_by_index.restype = POINTER(c_int) pa_context_set_sink_mute_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_suspend_sink_by_index = p.pa_context_suspend_sink_by_index pa_context_suspend_sink_by_index.restype = POINTER(c_int) pa_context_suspend_sink_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_sink_port_by_index = p.pa_context_set_sink_port_by_index pa_context_set_sink_port_by_index.restype = POINTER(c_int) pa_context_set_sink_port_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_sink_input_mute = p.pa_context_set_sink_input_mute pa_context_set_sink_input_mute.restype = POINTER(c_int) pa_context_set_sink_input_mute.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_sink_volume_by_index = p.pa_context_set_sink_volume_by_index pa_context_set_sink_volume_by_index.restype = POINTER(c_int) pa_context_set_sink_volume_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_sink_input_volume = p.pa_context_set_sink_input_volume pa_context_set_sink_input_volume.restype = POINTER(c_int) pa_context_set_sink_input_volume.argtypes = [ POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_move_sink_input_by_index = p.pa_context_move_sink_input_by_index pa_context_move_sink_input_by_index.restype = POINTER(c_int) pa_context_move_sink_input_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_default_sink = p.pa_context_set_default_sink pa_context_set_default_sink.restype = POINTER(PA_OPERATION) pa_context_set_default_sink.argtypes = [ POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_kill_sink_input = p.pa_context_kill_sink_input pa_context_kill_sink_input.restype = POINTER(PA_OPERATION) pa_context_kill_sink_input.argtypes = [ POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_kill_client = p.pa_context_kill_client pa_context_kill_client.restype = POINTER(PA_OPERATION) pa_context_kill_client.argtypes = [ POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_get_source_output_info = p.pa_context_get_source_output_info pa_context_get_source_output_info.restype = POINTER(c_int) pa_context_get_source_output_info.argtypes = [ POINTER(PA_CONTEXT), c_uint32, PA_SOURCE_OUTPUT_INFO_CB_T, c_void_p ] pa_context_get_source_output_info_list = p.pa_context_get_source_output_info_list pa_context_get_source_output_info_list.restype = POINTER(c_int) pa_context_get_source_output_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_SOURCE_OUTPUT_INFO_CB_T, c_void_p ] pa_context_move_source_output_by_index = p.pa_context_move_source_output_by_index pa_context_move_source_output_by_index.restype = POINTER(c_int) pa_context_move_source_output_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_source_output_volume = p.pa_context_set_source_output_volume pa_context_set_source_output_volume.restype = POINTER(c_int) pa_context_set_source_output_volume.argtypes = [ POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_source_output_mute = p.pa_context_set_source_output_mute pa_context_set_source_output_mute.restype = POINTER(c_int) pa_context_set_source_output_mute.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_get_source_info_by_index = p.pa_context_get_source_info_by_index pa_context_get_source_info_by_index.restype = POINTER(c_int) pa_context_get_source_info_by_index.argtypes = [ POINTER(PA_CONTEXT), PA_SOURCE_INFO_CB_T, c_void_p ] pa_context_get_source_info_list = p.pa_context_get_source_info_list pa_context_get_source_info_list.restype = POINTER(c_int) pa_context_get_source_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_SOURCE_INFO_CB_T, c_void_p ] pa_context_set_source_volume_by_index = p.pa_context_set_source_volume_by_index pa_context_set_source_volume_by_index.restype = POINTER(c_int) pa_context_set_source_volume_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_source_volume_by_index = p.pa_context_set_source_volume_by_index pa_context_set_source_volume_by_index.restype = POINTER(c_int) pa_context_set_source_volume_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_source_mute_by_index = p.pa_context_set_source_mute_by_index pa_context_set_source_mute_by_index.restype = POINTER(c_int) pa_context_set_source_mute_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_suspend_source_by_index = p.pa_context_suspend_source_by_index pa_context_suspend_source_by_index.restype = POINTER(c_int) pa_context_suspend_source_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_source_port_by_index = p.pa_context_set_source_port_by_index pa_context_set_source_port_by_index.restype = POINTER(c_int) pa_context_set_source_port_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_set_default_source = p.pa_context_set_default_source pa_context_set_default_source.restype = POINTER(PA_OPERATION) pa_context_set_default_source.argtypes = [ POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_kill_source_output = p.pa_context_kill_source_output pa_context_kill_source_output.restype = POINTER(PA_OPERATION) pa_context_kill_source_output.argtypes = [ POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_get_client_info_list = p.pa_context_get_client_info_list pa_context_get_client_info_list.restype = POINTER(c_int) pa_context_get_client_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_CLIENT_INFO_CB_T, c_void_p ] pa_context_get_card_info_by_index = p.pa_context_get_card_info_by_index pa_context_get_card_info_by_index.restype = POINTER(PA_OPERATION) pa_context_get_card_info_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, PA_CARD_INFO_CB_T, c_void_p ] pa_context_get_card_info_list = p.pa_context_get_card_info_list pa_context_get_card_info_list.restype = POINTER(PA_OPERATION) pa_context_get_card_info_list.argtypes = [ POINTER(PA_CONTEXT), PA_CARD_INFO_CB_T, c_void_p ] pa_context_set_card_profile_by_index = p.pa_context_set_card_profile_by_index pa_context_set_card_profile_by_index.restype = POINTER(c_int) pa_context_set_card_profile_by_index.argtypes = [ POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p ] pa_context_get_server_info = p.pa_context_get_server_info pa_context_get_server_info.restype = POINTER(PA_OPERATION) pa_context_get_server_info.argtypes = [ POINTER(PA_CONTEXT), PA_SERVER_INFO_CB_T, c_void_p ] # ^ bindings ######################################################################################### # v lib class PulsePort(): def __init__(self, pa_port): self.name = pa_port.name self.description = pa_port.description self.priority = pa_port.priority def debug(self): pprint(vars(self)) class PulseServer(): def __init__(self, pa_server): self.default_sink_name = pa_server.default_sink_name self.default_source_name = pa_server.default_source_name self.server_version = pa_server.server_version def debug(self): pprint(vars(self)) class PulseCardProfile(): def __init__(self, pa_profile): self.name = pa_profile.name self.description = pa_profile.description def debug(self): pprint(vars(self)) class PulseCard(): def __init__(self, pa_card): self.name = pa_card.name self.description = pa_proplist_gets(pa_card.proplist, b'device.description') self.index = pa_card.index self.driver = pa_card.driver self.owner_module = pa_card.owner_module self.n_profiles = pa_card.n_profiles self.profiles = [PulseCardProfile(pa_card.profiles[n]) for n in range(self.n_profiles)] self.active_profile = PulseCardProfile(pa_card.active_profile[0]) self.volume = type('volume', (object,), {'channels': 1, 'values': [0, 0]}) def debug(self): pprint(vars(self)) def __str__(self): return "Card-ID: {}, Name: {}".format(self.index, self.name.decode()) class PulseClient(): def __init__(self, pa_client): self.index = getattr(pa_client, "index", 0) self.name = getattr(pa_client, "name", pa_client) self.driver = getattr(pa_client, "driver", "default driver") self.owner_module = getattr(pa_client, "owner_module", -1) def debug(self): pprint(vars(self)) def __str__(self): return "Client-name: {}".format(self.name.decode()) class Pulse(): def __init__(self, client_name='libpulse', server_name=None): self.ret = None self.error = None self.operation = None self.connected = True self.action_done = False self.data = [] self.client_name = client_name.encode() self.server_name = server_name self.pa_signal_cb = PA_SIGNAL_CB_T(self.signal_cb) self.pa_state_cb = PA_STATE_CB_T(self.state_cb) self.mainloop = pa_mainloop_new() self.mainloop_api = pa_mainloop_get_api(self.mainloop) assert pa_signal_init(self.mainloop_api) == 0, "pa_signal_init failed" pa_signal_new(2, self.pa_signal_cb, None) pa_signal_new(15, self.pa_signal_cb, None) self.context = pa_context_new(self.mainloop_api, self.client_name) pa_context_set_state_callback(self.context, self.pa_state_cb, None) if pa_context_connect(self.context, self.server_name, 0, None) < 0: self.disconnect() sys.exit("Failed to connect to pulseaudio daemon: Connection refused") self.iterate() def reconnect(self): self.connected = False while not self.connected: self.error = None self.disconnect() self.mainloop = pa_mainloop_new() self.mainloop_api = pa_mainloop_get_api(self.mainloop) self.context = pa_context_new(self.mainloop_api, self.client_name) pa_context_set_state_callback(self.context, self.pa_state_cb, None) try: if pa_context_connect(self.context, self.server_name, 0, None) >= 0: self.iterate() self.connected = True except: pass sleep(0.5) def unmute_stream(self, obj): if type(obj) is PulseSinkInfo: self.sink_mute(obj.index, 0) elif type(obj) is PulseSinkInputInfo: self.sink_input_mute(obj.index, 0) elif type(obj) is PulseSourceInfo: self.source_mute(obj.index, 0) elif type(obj) is PulseSourceOutputInfo: self.source_output_mute(obj.index, 0) obj.mute = 0 def mute_stream(self, obj): if type(obj) is PulseSinkInfo: self.sink_mute(obj.index, 1) elif type(obj) is PulseSinkInputInfo: self.sink_input_mute(obj.index, 1) elif type(obj) is PulseSourceInfo: self.source_mute(obj.index, 1) elif type(obj) is PulseSourceOutputInfo: self.source_output_mute(obj.index, 1) obj.mute = 1 def set_volume(self, obj, volume): if type(obj) is PulseSinkInfo: self.set_sink_volume(obj.index, volume) elif type(obj) is PulseSinkInputInfo: self.set_sink_input_volume(obj.index, volume) elif type(obj) is PulseSourceInfo: self.set_source_volume(obj.index, volume) elif type(obj) is PulseSourceOutputInfo: self.set_source_output_volume(obj.index, volume) obj.volume = volume def change_volume_mono(self, obj, inc): obj.volume.values = [v + inc for v in obj.volume.values] self.set_volume(obj, obj.volume) def get_volume_mono(self, obj): return int(sum(obj.volume.values) / len(obj.volume.values)) def fill_clients(self): if not self.data: return data, self.data = self.data, [] clist = self.client_list() for d in data: for c in clist: if c.index == d.client_id: d.client = c break return data def signal_cb(self, api, e, sig, userdata): if sig == 2 or sig == 15: self.disconnect() return 0 def state_cb(self, c, b): state = pa_context_get_state(c) if state == PA_CONTEXT_READY: self.complete_action() elif state in (PA_CONTEXT_FAILED, PA_CONTEXT_TERMINATED): self.error = RuntimeError("Failed to complete action: {}, {}".format(state, pa_context_errno(c))) self.complete_action() return 0 def _action_cb(func): def wrapper(self, c, info, eof, userdata): if eof: self.complete_action() return 0 func(self, c, info, eof, userdata) return 0 return wrapper @_action_cb def card_cb(self, c, card_info, eof, userdata): self.data.append(PulseCard(card_info[0])) @_action_cb def client_cb(self, c, client_info, eof, userdata): self.data.append(PulseClient(client_info[0])) @_action_cb def sink_input_cb(self, c, sink_input_info, eof, userdata): self.data.append(PulseSinkInputInfo(sink_input_info[0])) @_action_cb def sink_cb(self, c, sink_info, eof, userdata): self.data.append(PulseSinkInfo(sink_info[0])) @_action_cb def source_output_cb(self, c, source_output_info, eof, userdata): self.data.append(PulseSourceOutputInfo(source_output_info[0])) @_action_cb def source_cb(self, c, source_info, eof, userdata): self.data.append(PulseSourceInfo(source_info[0])) def server_cb(self, c, server_info, eof): self.data.append(PulseServer(server_info[0])) self.complete_action() def context_success(self, c, success, userdata): self.complete_action() return 0 def complete_action(self): self.action_done = True def start_action(self): self.action_done = False def disconnect(self): pa_context_disconnect(self.context) pa_context_unref(self.context) pa_mainloop_free(self.mainloop) def sink_input_list(self): CB = PA_SINK_INPUT_INFO_CB_T(self.sink_input_cb) self.operation = pa_context_get_sink_input_info_list(self.context, CB, None) self.iterate() data, self.data = self.fill_clients(), [] return data or [] def sink_list(self): CB = PA_SINK_INFO_CB_T(self.sink_cb) self.operation = pa_context_get_sink_info_list(self.context, CB, None) self.iterate() data, self.data = self.data, [] return data or [] def source_output_list(self): CB = PA_SOURCE_OUTPUT_INFO_CB_T(self.source_output_cb) self.operation = pa_context_get_source_output_info_list(self.context, CB, None) self.iterate() data, self.data = self.fill_clients(), [] return data or [] def source_list(self): CB = PA_SOURCE_INFO_CB_T(self.source_cb) self.operation = pa_context_get_source_info_list(self.context, CB, None) self.iterate() data, self.data = self.data, [] return data or [] def get_server_info(self): CB = PA_SERVER_INFO_CB_T(self.server_cb) self.operation = pa_context_get_server_info(self.context, CB, None) self.iterate() data, self.data = self.data, [] return data[0] or None def card_list(self): CB = PA_CARD_INFO_CB_T(self.card_cb) self.operation = pa_context_get_card_info_list(self.context, CB, None) self.iterate() data, self.data = self.data, [] return data or [] def client_list(self): CB = PA_CLIENT_INFO_CB_T(self.client_cb) self.operation = pa_context_get_client_info_list(self.context, CB, None) self.iterate() data, self.data = self.data, [] return data or [] def sink_input_mute(self, index, mute): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_sink_input_mute(self.context, index, mute, CONTEXT, None) self.iterate() def sink_input_move(self, index, s_index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_move_sink_input_by_index(self.context, index, s_index, CONTEXT, None) self.iterate() def sink_mute(self, index, mute): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_sink_mute_by_index(self.context, index, mute, CONTEXT, None) self.iterate() def set_sink_input_volume(self, index, vol): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_sink_input_volume(self.context, index, vol.to_c(), CONTEXT, None) self.iterate() def set_sink_volume(self, index, vol): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_sink_volume_by_index(self.context, index, vol.to_c(), CONTEXT, None) self.iterate() def sink_suspend(self, index, suspend): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_suspend_sink_by_index(self.context, index, suspend, CONTEXT, None) self.iterate() def set_default_sink(self, name): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_default_sink(self.context, name, CONTEXT, None) self.iterate() def kill_sink(self, index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_kill_sink_input(self.context, index, CONTEXT, None) self.iterate() def kill_client(self, index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_kill_client(self.context, index, CONTEXT, None) self.iterate() def set_sink_port(self, index, port): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_sink_port_by_index(self.context, index, port, CONTEXT, None) self.iterate() def set_source_output_volume(self, index, vol): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_source_output_volume(self.context, index, vol.to_c(), CONTEXT, None) self.iterate() def set_source_volume(self, index, vol): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_source_volume_by_index(self.context, index, vol.to_c(), CONTEXT, None) self.iterate() def source_suspend(self, index, suspend): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_suspend_source_by_index(self.context, index, suspend, CONTEXT, None) self.iterate() def set_default_source(self, name): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_default_source(self.context, name, CONTEXT, None) self.iterate() def kill_source(self, index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_kill_source_output(self.context, index, CONTEXT, None) self.iterate() def set_source_port(self, index, port): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_source_port_by_index(self.context, index, port, CONTEXT, None) self.iterate() def source_output_mute(self, index, mute): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_source_output_mute(self.context, index, mute, CONTEXT, None) self.iterate() def source_mute(self, index, mute): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_source_mute_by_index(self.context, index, mute, CONTEXT, None) self.iterate() def source_output_move(self, index, s_index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_move_source_output_by_index(self.context, index, s_index, CONTEXT, None) self.iterate() def set_card_profile(self, index, p_index): CONTEXT = PA_CONTEXT_SUCCESS_CB_T(self.context_success) self.operation = pa_context_set_card_profile_by_index(self.context, index, p_index, CONTEXT, None) self.iterate() def run(self): self.ret = pointer(c_int(0)) pa_mainloop_run(self.mainloop, self.ret) def iterate(self, times=1, start=True): if self.error: raise self.error start and self.start_action() self.ret = pointer(c_int()) pa_mainloop_iterate(self.mainloop, times, self.ret) while not self.action_done: pa_mainloop_iterate(self.mainloop, times, self.ret) class PulseSink(): def __init__(self, sink_info): self.index = sink_info.index self.name = sink_info.name self.mute = sink_info.mute self.volume = PulseVolume(sink_info.volume) def debug(self): pprint(vars(self)) class PulseSinkInfo(PulseSink): def __init__(self, pa_sink_info): PulseSink.__init__(self, pa_sink_info) self.description = pa_sink_info.description self.owner_module = pa_sink_info.owner_module self.driver = pa_sink_info.driver self.monitor_source = pa_sink_info.monitor_source self.monitor_source_name = pa_sink_info.monitor_source_name self.n_ports = pa_sink_info.n_ports self.ports = [PulsePort(pa_sink_info.ports[i].contents) for i in range(self.n_ports)] self.active_port = None if self.n_ports: self.active_port = PulsePort(pa_sink_info.active_port.contents) def __str__(self): return "ID: {}, Name: {}, Mute: {}, {}".format( self.index, self.description.decode(), self.mute, self.volume) class PulseSinkInputInfo(PulseSink): def __init__(self, pa_sink_input_info): PulseSink.__init__(self, pa_sink_input_info) self.owner_module = pa_sink_input_info.owner_module self.client = PulseClient(pa_sink_input_info.name) self.client_id = pa_sink_input_info.client self.sink = self.owner = pa_sink_input_info.sink self.driver = pa_sink_input_info.driver self.media_name = pa_proplist_gets(pa_sink_input_info.proplist, b'media.name') def __str__(self): if self.client: return "ID: {}, Name: {}, Mute: {}, {}".format( self.index, self.client.name.decode(), self.mute, self.volume) return "ID: {}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute) class PulseSource(): def __init__(self, source_info): self.index = source_info.index self.name = source_info.name self.mute = source_info.mute self.volume = PulseVolume(source_info.volume) def debug(self): pprint(vars(self)) class PulseSourceInfo(PulseSource): def __init__(self, pa_source_info): PulseSource.__init__(self, pa_source_info) self.description = pa_source_info.description self.owner_module = pa_source_info.owner_module self.monitor_of_sink = pa_source_info.monitor_of_sink self.monitor_of_sink_name = pa_source_info.monitor_of_sink_name self.driver = pa_source_info.driver self.n_ports = pa_source_info.n_ports self.ports = [PulsePort(pa_source_info.ports[i].contents) for i in range(self.n_ports)] self.active_port = None if self.n_ports: self.active_port = PulsePort(pa_source_info.active_port.contents) def __str__(self): return "ID: {}, Name: {}, Mute: {}, {}".format( self.index, self.description.decode(), self.mute, self.volume) class PulseSourceOutputInfo(PulseSource): def __init__(self, pa_source_output_info): PulseSource.__init__(self, pa_source_output_info) self.owner_module = pa_source_output_info.owner_module self.client = PulseClient(pa_source_output_info.name) self.client_id = pa_source_output_info.client self.source = self.owner = pa_source_output_info.source self.driver = pa_source_output_info.driver def __str__(self): if self.client: return "ID: {}, Name: {}, Mute: {}, {}".format( self.index, self.client.name.decode(), self.mute, self.volume) return "ID: {}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute) class PulseVolume(): def __init__(self, cvolume): self.channels = cvolume.channels self.values = [(round(x * 100 / PA_VOLUME_NORM)) for x in cvolume.values[:self.channels]] def to_c(self): self.values = list(map(lambda x: max(min(x, 150), 0), self.values)) cvolume = PA_CVOLUME() cvolume.channels = self.channels for x in range(self.channels): cvolume.values[x] = round((self.values[x] * PA_VOLUME_NORM) / 100) return cvolume def debug(self): pprint(vars(self)) def __str__(self): return "Channels: {}, Volumes: {}".format(self.channels, [str(x) + "%" for x in self.values]) # ^ lib ######################################################################################### # v main class Bar(): # should be in correct order LEFT, RIGHT, RLEFT, RRIGHT, CENTER, SUB, SLEFT, SRIGHT, NONE = range(9) def __init__(self, pa): if type(pa) is str: self.name = pa return if type(pa) in (PulseSinkInfo, PulseSourceInfo, PulseCard): self.fullname = pa.description.decode() else: self.fullname = pa.client.name.decode() self.name = re.sub('^ALSA plug-in \[|\]$', '', self.fullname.replace('|', ' ')) self.index = pa.index self.owner = -1 self.stream_index = -1 self.poll_data(pa, 0, 0) self.maxsize = 150 self.locked = True def poll_data(self, pa, owned, stream_index): self.channels = pa.volume.channels self.muted = getattr(pa, 'mute', False) self.owned = owned self.stream_index = stream_index self.volume = pa.volume.values try: self.media_name = ': {}'.format(pa.media_name.decode().replace('|', ' ')) except: self.media_name = '' if type(pa) in (PulseSinkInputInfo, PulseSourceOutputInfo): self.owner = pa.owner self.pa = pa def mute_toggle(self): pulse.unmute_stream(self.pa) if self.muted else pulse.mute_stream(self.pa) def lock_toggle(self): self.locked = not self.locked def set(self, n, side): vol = self.pa.volume if self.locked: for i, _ in enumerate(vol.values): vol.values[i] = n else: vol.values[side] = n pulse.set_volume(self.pa, vol) def move(self, n, side): vol = self.pa.volume if self.locked: for i, _ in enumerate(vol.values): vol.values[i] += n else: vol.values[side] += n pulse.set_volume(self.pa, vol) class Screen(): DOWN = 1 UP = -1 SCROLL_UP = [getattr(curses, i, 0) for i in ['BUTTON4_PRESSED', 'BUTTON3_TRIPLE_CLICKED']] SCROLL_DOWN = [getattr(curses, i, 0) for i in ['BUTTON5_PRESSED', 'A_LOW', 'A_BOLD', 'BUTTON4_DOUBLE_CLICKED']] KEY_MOUSE = getattr(curses, 'KEY_MOUSE', 0) SPACE_KEY = 32 ESC_KEY = 27 MODE = {0: 1, 1: 0, 2: 0} DIGITS = list(map(ord, map(str, range(10)))) SIDES = {Bar.LEFT: 'Left', Bar.RIGHT: 'Right', Bar.RLEFT: 'Rear Left', Bar.RRIGHT: 'Rear Right', Bar.CENTER: 'Center', Bar.SUB: 'Subwoofer', Bar.SLEFT: 'Side left', Bar.SRIGHT: 'Side right'} SEQ_TO_KEY = { (91, 49, 59, 50, 67): curses.KEY_SRIGHT, (91, 49, 59, 50, 68): curses.KEY_SLEFT, (79, 80, -1, -1, -1): curses.KEY_F1, (79, 81, -1, -1, -1): curses.KEY_F2, (79, 82, -1, -1, -1): curses.KEY_F3, } def __init__(self, color=3, mouse=True): environ['ESCDELAY'] = '25' self.screen = curses.initscr() self.screen.timeout(900) self.screen.scrollok(1) if mouse: try: curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.BUTTON1_CLICKED | self.KEY_MOUSE | functools.reduce(operator.or_, list(self.SCROLL_UP)) | functools.reduce(operator.or_, list(self.SCROLL_DOWN))) except: self.KEY_MOUSE = 0 else: self.KEY_MOUSE = 0 try: curses.curs_set(0) except: # terminal doesn't support visibility requests pass self.screen.border(0) self.index = 0 self.top_line_num = 0 self.focus_line_num = 0 self.lines, self.cols = curses.LINES - 1, curses.COLS - 1 self.info, self.menu = str, str self.menu_titles = ['F1 Output', 'F2 Input', 'F3 Cards'] self.data = [] self.modes = [[[], 0, 0] for i in range(6)] self.active_mode = 0 self.old_mode = 0 self.change_mode_allowed = True self.n_lines, self.n_lines_info = 0, 0 self.color_mode = color if color in (1, 2) and curses.has_colors(): curses.start_color() curses.use_default_colors() curses.init_pair(1, curses.COLOR_GREEN, -1) curses.init_pair(2, curses.COLOR_YELLOW, -1) curses.init_pair(3, curses.COLOR_RED, -1) self.green = curses.color_pair(1) self.yellow = curses.color_pair(2) self.red = curses.color_pair(3) n = 7 if curses.COLORS < 256 else 67 curses.init_pair(n, n - 1, -1) self.muted_color = curses.color_pair(n) if curses.COLORS < 256: self.gray_gradient = [curses.A_NORMAL] * 3 else: try: curses.init_pair(240, 240, -1) curses.init_pair(243, 243, -1) curses.init_pair(246, 246, -1) self.gray_gradient = [ curses.color_pair(240), curses.color_pair(243), curses.color_pair(246), ] except: self.gray_gradient = [curses.A_NORMAL] * 3 else: # if term has colors start them regardless of --color to avoid weird backgrounds # on some terminals if curses.has_colors(): curses.start_color() curses.use_default_colors() self.gray_gradient = [curses.A_NORMAL] * 3 self.green = self.yellow = self.red = self.muted_color = curses.A_NORMAL self.gradient = [self.green, self.yellow, self.red] self.submenu_data = [] self.submenu_width = 30 self.submenu_show = False self.submenu = curses.newwin(curses.LINES, 0, 0, 0) self.helpwin_show = False self.helpwin = curses.newwin(13, 62, 0, 0) try: self.helpwin.mvwin((curses.LINES // 2) - 6, (curses.COLS // 2) - 31) except: pass self.selected = None self.action = None def display_line(self, index, line, mod=curses.A_NORMAL): shift = 0 for s in line.split('\n'): p = s.rsplit('|') p1 = ''.join(p[:-1]) try: self.screen.addstr(index, shift, p1, int(p[-1]) | mod) except: self.screen.addstr(min(curses.LINES - 1, index), min(curses.COLS - 1, shift), p1, int(p[-1]) | mod) shift += len(p1) def change_mode(self, mode): if not self.change_mode_allowed: return self.modes[self.active_mode][1] = self.focus_line_num self.modes[self.active_mode][2] = self.top_line_num self.old_mode = self.active_mode self.MODE = self.MODE.fromkeys(self.MODE, 0) self.MODE[mode] = 1 self.focus_line_num = self.modes[mode][1] self.top_line_num = self.modes[mode][2] self.active_mode = mode self.get_data() def next_mode(self): for mode, active in self.MODE.items(): if active == 1: self.change_mode((1 + mode) % 3) return def update_menu(self): if self.change_mode_allowed: self.menu = '{}|{}\n {}|{}\n {}|{}\n {:>{}}|{}'.format( self.menu_titles[0], curses.A_BOLD if self.MODE[0] else curses.A_DIM, self.menu_titles[1], curses.A_BOLD if self.MODE[1] else curses.A_DIM, self.menu_titles[2], curses.A_BOLD if self.MODE[2] else curses.A_DIM, "? - help", self.cols - 30, curses.A_DIM) else: select = 'output' if type(self.selected[0].pa) is PulseSinkInputInfo else 'input' self.menu = "Select new {} device:|{}".format(select, curses.A_NORMAL) def update_info(self): focus = self.focus_line_num + self.top_line_num + 1 try: bar, side = self.data[focus - 1][0], self.data[focus - 1][1] except IndexError: self.focus_line_num, self.top_line_num = 0, 0 [self.scroll(self.UP) for _ in range(len(self.data))] return if side is Bar.NONE: self.info = str return side = 'All' if bar.locked else 'Mono' if bar.channels == 1 else self.SIDES[side] pos = int(focus * 100 / self.n_lines) name = '{}: {}'.format(bar.fullname, side) if len(name) > self.cols - 14: name = '{}: {}'.format(bar.fullname[:self.cols - (18 + len(side))], side) self.info = '{}|{}\n{}|{}\n{}|{}\n'.format( "L ", self.red if bar.locked else curses.A_DIM, "M ", self.red if bar.muted else curses.A_DIM, name, curses.A_NORMAL) self.info += '{:>{}}%|{}'.format(pos, self.cols - len(name) - 6, curses.A_BOLD) def check_resize(self): if curses.is_term_resized(curses.LINES, curses.COLS): self.screen.erase() y, x = self.screen.getmaxyx() curses.resizeterm(y, x) self.submenu.resize(curses.LINES, self.submenu_width + 1) if self.submenu_show: self.submenu_show = False self.focus_line_num = self.modes[5][1] self.top_line_num = self.modes[5][2] self.helpwin.resize(13, 62) self.helpwin.mvwin((curses.LINES // 2) - 6, (curses.COLS // 2) - 31) self.helpwin_show = False self.screen.refresh() self.lines = curses.LINES - 2 self.cols = curses.COLS - 1 def run_mouse(self): try: _, x, y, _, c = curses.getmouse() if c & curses.BUTTON1_CLICKED: if y > 0: top, bottom = self.top_line_num, len(self.data[self.top_line_num:self.top_line_num + self.lines]) - 1 if y - 1 <= bottom: self.focus_line_num = max(top, min(bottom, y - 1)) else: f1 = len(self.menu_titles[0]) + 1 # 1 is 'spacing' after the title f2 = f1 + len(self.menu_titles[1]) + 2 f3 = f2 + len(self.menu_titles[2]) + 3 if x in range(0, f1): self.change_mode(0) elif x in range(f1, f2): self.change_mode(1) elif x in range(f2, f3): self.change_mode(2) return c except curses.error: return None def sigint(self): # if ^C pressed while doing pulse.reconnect wrapper.restore won't be called # so have to restore it manually here self.screen.keypad(0) curses.echo() curses.nocbreak() curses.endwin() sys.exit(0) def run(self, scr): signal.signal(signal.SIGINT, lambda signal, frame: self.sigint()) from ctypes import _reset_cache as reset_cache while True: try: self.check_resize() if not self.submenu_show: try: self.get_data() except RuntimeError: self.focus_line_num = 0 self.data = [(Bar('PA - Connection refused.\nTrying to reconnect.'), Bar.NONE, 0)] self.display() pulse.reconnect() continue self.update_menu() self.update_info() self.display() if self.helpwin_show: self.display_helpwin() self.run_helpwin() continue elif self.change_mode_allowed: self.display_submenu() self.run_submenu() continue except (curses.error, IndexError, ValueError) as e: self.screen.erase() self.screen.addstr("Terminal *might* be too small {}:{}\n".format(curses.LINES, curses.COLS)) self.screen.addstr("{}\n{}\n".format(str(self.MODE), str(e))) self.screen.addstr(str(traceback.extract_tb(e.__traceback__))) focus = self.top_line_num + self.focus_line_num bar, side = self.data[focus][0], self.data[focus][1] c = self.screen.getch() if c == -1: continue elif c == self.KEY_MOUSE: c = self.run_mouse() or c elif c == 27: # translating key-sequences to keys recognized by curses l = [] self.screen.timeout(0) for i in range(5): l.append(self.screen.getch()) self.screen.timeout(900) c = self.SEQ_TO_KEY.get(tuple(l), 27) if c == curses.KEY_F1: self.change_mode(0) elif c == curses.KEY_F2: self.change_mode(1) elif c == curses.KEY_F3: self.change_mode(2) elif c == ord('?'): self.helpwin_show = True elif c == ord('\n'): if not self.submenu_show and self.change_mode_allowed and side != Bar.NONE: self.selected = self.data[focus] if type(self.selected[0].pa) in (PulseSinkInfo, PulseSourceInfo): self.submenu_data = ['Suspend', 'Resume', 'Set as default'] if self.selected[0].pa.n_ports: self.submenu_data.append('Set port') elif type(self.selected[0].pa) is PulseCard: self.submenu_data = [p.description.decode() for p in self.selected[0].pa.profiles] else: self.submenu_data = ['Move', 'Kill'] self.submenu_show = True self.modes[5][0] = 0 self.modes[5][1] = self.focus_line_num self.modes[5][2] = self.top_line_num self.focus_line_num = self.top_line_num = 0 self.n_lines = len(self.submenu_data) self.submenu_width = min(self.cols - 2, max(30, len(max(self.submenu_data, key=len)) + 5)) self.submenu_data = [s[:self.submenu_width - 4] for s in self.submenu_data] elif not self.change_mode_allowed: self.submenu_show = False self.change_mode_allowed = True if self.action == 'Move': if type(self.selected[0].pa) is PulseSinkInputInfo: pulse.sink_input_move(self.selected[0].index, self.data[focus][0].pa.index) elif type(self.selected[0].pa) is PulseSourceOutputInfo: pulse.source_output_move(self.selected[0].index, self.data[focus][0].pa.index) self.change_mode(self.old_mode) self.focus_line_num = self.modes[5][1] self.top_line_num = self.modes[5][2] else: self.change_mode(self.old_mode) elif c == ord('\t'): self.next_mode() elif c == ord('q') or c == self.ESC_KEY: if not self.change_mode_allowed: self.submenu_show = False self.change_mode_allowed = True self.change_mode(self.old_mode) self.focus_line_num = self.modes[5][1] self.top_line_num = self.modes[5][2] elif self.helpwin_show: self.helpwin_show = False else: sys.exit() if side is Bar.NONE: continue if c == curses.KEY_UP or c == ord('k'): if bar.locked: if self.data[focus][1] == 0: n = 1 else: n = self.data[focus][1] + 1 [self.scroll(self.UP) for _ in range(n)] else: self.scroll(self.UP) if not self.data[self.top_line_num + self.focus_line_num][0]: self.scroll(self.UP) elif c == curses.KEY_DOWN or c == ord('j'): if bar.locked: if self.data[focus][1] == self.data[focus][3] - 1: n = 1 else: n = ((self.data[focus][3] - 1) - self.data[focus][1]) + 1 [self.scroll(self.DOWN) for _ in range(n)] else: self.scroll(self.DOWN) if not self.data[self.top_line_num + self.focus_line_num][0]: self.scroll(self.DOWN) elif c == ord('m'): bar.mute_toggle() elif c == self.SPACE_KEY: bar.lock_toggle() elif c == curses.KEY_LEFT or c == ord('h') or any([c & i for i in self.SCROLL_DOWN]): bar.move(-1, side) elif c == curses.KEY_RIGHT or c == ord('l') or any([c & i for i in self.SCROLL_UP]): bar.move(1, side) elif c == curses.KEY_SLEFT or c == ord('H'): bar.move(-10, side) elif c == curses.KEY_SRIGHT or c == ord('L'): bar.move(10, side) elif c in self.DIGITS: percent = int(chr(c)) * 10 bar.set(100 if percent == 0 else percent, side) def run_submenu(self): c = self.screen.getch() if c == ord('q') or c == self.ESC_KEY: self.submenu_show = False self.focus_line_num = self.modes[5][1] self.top_line_num = self.modes[5][2] elif c == curses.KEY_UP or c == ord('k'): self.scroll(self.UP) elif c == curses.KEY_DOWN or c == ord('j'): self.scroll(self.DOWN) elif c == ord('\n'): focus = self.focus_line_num + self.top_line_num self.action = self.submenu_data[focus] if self.action == 'Move': if self.active_mode == 0: self.change_mode(3) elif self.active_mode == 1: self.change_mode(4) self.change_mode_allowed = self.submenu_show = False return elif self.action == 'Kill': try: pulse.kill_client(self.selected[0].pa.client.index) except: if type(self.selected[0].pa) is PulseSinkInputInfo: pulse.kill_sink(self.selected[2]) else: pulse.kill_source(self.selected[2]) elif self.action == 'Suspend': if type(self.selected[0].pa) is PulseSinkInfo: pulse.sink_suspend(self.selected[2], 1) else: pulse.source_suspend(self.selected[2], 1) elif self.action == 'Resume': if type(self.selected[0].pa) is PulseSinkInfo: pulse.sink_suspend(self.selected[2], 0) else: pulse.source_suspend(self.selected[2], 0) elif self.action == 'Set as default': if type(self.selected[0].pa) is PulseSinkInfo: pulse.set_default_sink(self.selected[0].pa.name) else: pulse.set_default_source(self.selected[0].pa.name) elif self.action == 'Set port': self.submenu_data = [] for i in self.selected[0].pa.ports: s = i.description.decode().strip('|') if len(s) > 26: s = s[:23] + '..' self.submenu_data.append(s) if self.selected[0].pa.active_port.name == i.name: self.submenu_data[-1] = " {}|{}".format(self.submenu_data[-1], self.red) self.focus_line_num = self.top_line_num = 0 self.n_lines = len(self.submenu_data) return else: index = self.selected[0].pa.index if type(self.selected[0].pa) is PulseSinkInfo: pulse.set_sink_port(index, self.selected[0].pa.ports[focus].name) elif type(self.selected[0].pa) is PulseSourceInfo: pulse.set_source_port(index, self.selected[0].pa.ports[focus].name) elif type(self.selected[0].pa) is PulseCard: pulse.set_card_profile(index, self.selected[0].pa.profiles[focus].name) self.change_mode_allowed = True self.submenu_show = False self.focus_line_num = self.modes[5][1] self.top_line_num = self.modes[5][2] def build(self, to, devices, streams): tmp = [] index = 0 for device in devices: index += device.volume.channels stream_index = device.volume.channels tmp.append([device, device.volume.channels, index, stream_index]) device_index = len(tmp) - 1 for stream in streams: if stream.owner == device.index: index += stream.volume.channels stream_index += stream.volume.channels tmp.append([stream, -1, index, stream_index]) tmp[device_index][1] += stream.volume.channels tmp[-1][1] = tmp[device_index][1] for s in tmp: found = False for i, data in enumerate(to): if s[0].index == data[2] and type(s[0]) == type(data[0].pa): found = True data[0].poll_data(s[0], s[1], s[3]) y = s[2] - (data[3] - data[1]) to[i], to[y] = to[y], to[i] if not found: bar = Bar(s[0]) bar.owned = s[1] bar.stream_index = s[3] for c in range(s[0].volume.channels): to.append((bar, c, s[0].index, s[0].volume.channels)) for i in reversed(range(len(to))): data = to[i] for s in tmp: if s[0].index == data[2] and type(s[0]) == type(data[0].pa): y = s[2] - (data[3] - data[1]) to[i], to[y] = to[y], to[i] break else: del to[i] if self.focus_line_num + self.top_line_num >= i: self.scroll(self.UP) return to def add_spacers(self, f): tmp = [] l = len(f) for i, s in enumerate(f): tmp.append(s) if s[0].stream_index == s[0].owned and s[1] == s[0].channels - 1 and i != l - 1: tmp.append((None, -1, 0, 0)) return tmp def get_data(self): if self.MODE[0]: self.data = self.build(self.modes[0][0], pulse.sink_list(), pulse.sink_input_list()) self.data = self.add_spacers(self.data) elif self.MODE[1]: self.data = self.build(self.modes[1][0], pulse.source_list(), pulse.source_output_list()) self.data = self.add_spacers(self.data) elif self.MODE[2]: self.data = self.build(self.modes[2][0], pulse.card_list(), []) elif type(self.selected[0].pa) is PulseSinkInputInfo: self.data = self.build(self.modes[3][0], pulse.sink_list(), []) elif type(self.selected[0].pa) is PulseSourceOutputInfo: self.data = self.build(self.modes[4][0], pulse.source_list(), []) self.server = pulse.get_server_info() self.n_lines = len(self.data) if not self.n_lines: self.focus_line_num = 0 self.data.append((Bar('no data'), Bar.NONE, 0)) if not self.data[self.top_line_num + self.focus_line_num][0]: self.scroll(self.UP) def display(self): self.screen.erase() top = self.top_line_num bottom = self.top_line_num + self.lines self.display_line(0, self.menu) for index, line in enumerate(self.data[top:bottom]): bar, bartype = line[0], line[1] if not bar: self.screen.addstr(index + 1, 0, '', curses.A_DIM) continue elif bartype is Bar.NONE: for i, name in enumerate(bar.name.split('\n')): self.screen.addstr((self.lines // 2) + i, (self.cols // 2) - len(name) // 2, name, curses.A_DIM) break # hightlight lines from same bar same, found = [], False for i, v in enumerate(self.data[top:bottom]): if v[0] is self.data[self.top_line_num + self.focus_line_num][0]: same.append(v[0]) tree = ' ' if bar.owner == -1 and bar.owned > bar.channels: tree = ' │' if bar.owner != -1: tree = ' │' if bartype == Bar.LEFT: if bar.owner == -1: tree = ' ' if bar.owner != -1: tree = ' ├─' if bar.stream_index == bar.owned: tree = ' └─' if bar.channels != 1: brackets = [BAR_TOP_LEFT, BAR_TOP_RIGHT] else: brackets = [BAR_LEFT_MONO, BAR_RIGHT_MONO] elif bartype == bar.channels - 1: if bar.stream_index == bar.owned: tree = ' ' brackets = [BAR_BOTTOM_LEFT, BAR_BOTTOM_RIGHT] else: if bar.stream_index == bar.owned: tree = ' ' brackets = ['├', '┤'] # focus current lines focus_hi, bracket_hi, arrow, gradient = 0, 0, ARROW, self.gradient if index == self.focus_line_num: focus_hi = bracket_hi = curses.A_BOLD arrow = ARROW_FOCUSED elif bar in same: focus_hi = curses.A_BOLD if bar.locked: bracket_hi = curses.A_BOLD arrow = ARROW_LOCKED elif not bar.muted and self.color_mode != 2: gradient = self.gray_gradient # highlight chosen sink/source or muted if not self.change_mode_allowed and self.selected[0].owner == self.data[index][0].index: bracket_hi = self.red | bracket_hi elif bar.muted: bracket_hi = bracket_hi | self.red focus_hi = focus_hi | self.muted_color off = 6 * (self.cols // (43 if self.cols <= 60 else 25)) - len(tree) cols = self.cols - 31 - off - len(tree) vol = list(BAR_OFF * (cols - (cols % 3 != 0))) n = int(len(vol) * bar.volume[bartype] / bar.maxsize) vol[:n] = BAR_ON * n vol = ''.join(vol) if bartype is Bar.LEFT: if bar.pa.name in (self.server.default_sink_name, self.server.default_source_name): tree = '*' name = '{}{}'.format(bar.name, bar.media_name) name = name[:20 + off] + '~' if len(name) > 20 + off else name line = '{:<{}}|{}\n {:<3}|{}\n '.format(name, 22 + off, focus_hi, '' if type(bar.pa) is PulseCard else bar.volume[0], focus_hi) elif bartype is Bar.RIGHT: line = '{:>{}}|{}\n {}|{}\n {:<3}|{}\n '.format( '', 21 + off, self.red if bar.locked else curses.A_DIM, '', self.red if bar.muted else curses.A_DIM, bar.volume[bartype], focus_hi) else: line = '{:>{}}{:<3}|{}\n '.format('', 23 + off, bar.volume[bartype], focus_hi) if type(bar.pa) is PulseCard: volbar = '\n{}|0'.format(bar.pa.active_profile.description.decode()[:len(vol)]) brackets = [' ', ' '] else: volbar = '' for i, v in enumerate(re.findall('.{{{}}}'.format((len(vol) // 3)), vol)): volbar += '\n{}|{}'.format(v, gradient[i] | focus_hi) line += '{:>1}|{}\n{}|{}{}\n{}|{}\n{}|{}'.format(arrow, curses.A_BOLD, brackets[0], bracket_hi, volbar, brackets[1], bracket_hi, arrow, curses.A_BOLD) self.display_line(index + 1, tree + "|0\n" + line) self.display_line(self.lines + 1, self.info) self.screen.refresh() def display_helpwin(self): '''h/j/k/l, arrows navigation, volume change H/L, Shift+Left/Shift+Right change volume by 10 1/2/3/4/5/6/7/8/9/0 set volume to 10%-100% m mute/unmute Space lock/unlock channels together Enter context menu F1/F2/F3 change modes Tab go to next mode Mouse left click select device or mode Mouse wheel volume change q/Esc/^C quit''' self.helpwin.erase() self.helpwin.border() for i, s in enumerate(self.display_helpwin.__doc__.split('\n')): self.helpwin.addstr(i + 1, 1, s.strip(), curses.A_NORMAL) self.helpwin.refresh() def run_helpwin(self): c = self.screen.getch() if c == ord('q') or c == self.ESC_KEY: self.helpwin_show = False def display_submenu(self): self.submenu.erase() top = self.top_line_num bottom = self.top_line_num + self.lines + 2 self.submenu.resize(curses.LINES, self.submenu_width + 1) self.submenu.vline(0, self.submenu_width, curses.ACS_VLINE, self.lines + 2) for index, line in enumerate(self.submenu_data[top:bottom]): if index == self.focus_line_num: focus_hi = curses.A_BOLD arrow = ARROW_FOCUSED else: focus_hi = curses.A_NORMAL arrow = ' ' if '|' in line: self.display_line(index, ' ' + arrow + line, focus_hi) else: self.submenu.addstr(index, 1, arrow + ' ' + line, focus_hi) self.submenu.refresh() def scroll(self, n): next_line_num = self.focus_line_num + n if n == self.UP and self.focus_line_num == 0 and self.top_line_num != 0: self.top_line_num += self.UP return elif n == self.DOWN and next_line_num == self.lines and\ (self.top_line_num + self.lines) != self.n_lines: self.top_line_num += self.DOWN return if n == self.UP and (self.top_line_num != 0 or self.focus_line_num != 0): self.focus_line_num = next_line_num elif n == self.DOWN and self.focus_line_num != self.lines and\ (self.top_line_num + self.focus_line_num + 1) != self.n_lines: self.focus_line_num = next_line_num def usage(): print(__doc__) pulse = None def main(): try: opts, args = getopt.getopt( sys.argv[1:], "hvl", ["help", "version", "list", "list-sinks", "list-sources", "id=", "set-volume=", "set-volume-all=", "change-volume=", "get-volume", "get-mute", "toggle-mute", "mute", "unmute", "color=", "server=", "no-mouse"]) except getopt.GetoptError as err: sys.exit("ERR: {}".format(err)) assert args == [], sys.exit('ERR: {} not not recognized'.format(' '.join(args).strip())) dopts = dict(opts) server = dopts.get('--server', '').encode() or None mouse = True if '--no-mouse' not in dopts else False try: color = min(2, max(0, int(dopts.get('--color', '') or 2))) except: sys.exit('ERR: color must be a number') global pulse pulse = Pulse('pulsemixer', server) noninteractive_opts = dict(dopts) noninteractive_opts.pop('--server', None) noninteractive_opts.pop('--color', None) noninteractive_opts.pop('--no-mouse', None) if not noninteractive_opts: if sys.stdout.isatty(): title = 'pulsemixer {}'.format(server.decode() if server else '') sys.stdout.write('\033]2;{}\007'.format(title.strip())) sys.stdout.flush() curses.wrapper(Screen(color, mouse).run) else: sys.exit('ERR: output is not a tty-like device') sinks = pulse.sink_list() sink_inputs = pulse.sink_input_list() sources = pulse.source_list() source_outputs = pulse.source_output_list() server_info = pulse.get_server_info() index = [s.index for s in sinks if s.name == server_info.default_sink_name][0] streams = {} for i in source_outputs + sources + sink_inputs + sinks: streams[i.index] = i check_id = lambda x: x in streams or sys.exit('ERR: No such ID: ' + str(x)) check_id(index) for opt, arg in opts: if opt in ('-h', '--help'): sys.exit(usage()) if opt in ('-v', '--version'): sys.exit(print(VERSION)) elif opt == '--id': try: index = int(arg) except: sys.exit('ERR: id must be a number') check_id(index) elif opt in ('-l', '--list'): for sink in sink_inputs: print("Sink input:\t", sink) for source in source_outputs: print("Source output:\t", source) for sink in sinks: print("Sink:\t\t", sink) for source in sources: print("Source:\t\t", source) elif opt == '--list-sinks': for sink in sink_inputs: print("Sink input:\t", sink) for sink in sinks: print("Sink:\t\t", sink) elif opt == '--list-sources': for source in source_outputs: print("Source output:\t", source) for source in sources: print("Source:\t\t", source) elif opt == '--get-mute': print(streams[index].mute) elif opt == '--mute': pulse.mute_stream(streams[index]) elif opt == '--unmute': pulse.unmute_stream(streams[index]) elif opt == '--toggle-mute': if streams[index].mute: pulse.unmute_stream(streams[index]) else: pulse.mute_stream(streams[index]) elif opt == '--get-volume': print(*streams[index].volume.values) elif opt == '--set-volume': vol = streams[index].volume for i, _ in enumerate(vol.values): vol.values[i] = int(arg) pulse.set_volume(streams[index], vol) elif opt == '--set-volume-all': vol = streams[index].volume arg = arg.strip(':').split(':') if len(arg) != len(vol.values): sys.exit("ERR: Specified volumes not equal to number of channles in the stream") for i, _ in enumerate(vol.values): vol.values[i] = int(arg[i]) pulse.set_volume(streams[index], vol) elif opt == '--change-volume': vol = streams[index].volume for i, _ in enumerate(vol.values): vol.values[i] += int(arg) pulse.set_volume(streams[index], vol) if __name__ == '__main__': main() pulsemixer-1.4.0/setup.py000066400000000000000000000005261321522055300154240ustar00rootroot00000000000000from setuptools import setup setup(name='pulsemixer', version='1.4.0', description='pulsemixer - cli and curses mixer for pulseaudio', url='https://github.com/GeorgeFilipkin/pulsemixer', author='George Filipkin', author_email='botebotebot@gmail.com', license='MIT', scripts=['pulsemixer'], )