pulsemixer 85 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056
  1. #!/usr/bin/env python3
  2. # this script is taken from https://github.com/GeorgeFilipkin/pulsemixer
  3. '''Usage of pulsemixer:
  4. -h, --help show this help message and exit
  5. -v, --version print version
  6. -l, --list list everything
  7. --list-sources list sources
  8. --list-sinks list sinks
  9. --id ID specify ID, default sink is used if no ID specified
  10. --get-volume get volume for ID
  11. --set-volume n set volume for ID
  12. --set-volume-all n:n set volume for ID, for every channel
  13. --change-volume +-n change volume for ID
  14. --max-volume n set volume to n if volume is higher than n
  15. --get-mute get mute for ID
  16. --mute mute ID
  17. --unmute unmute ID
  18. --toggle-mute toggle mute for ID
  19. --server choose the server to connect to
  20. --color n 0 no color, 1 color currently selected, 2 full-color
  21. --no-mouse disable mouse support
  22. --create-config generate configuration file'''
  23. VERSION = '1.5.1'
  24. import curses
  25. import functools
  26. import getopt
  27. import operator
  28. import os
  29. import re
  30. import signal
  31. import sys
  32. import threading
  33. import traceback
  34. from collections import OrderedDict
  35. from configparser import ConfigParser
  36. from ctypes import *
  37. from itertools import takewhile
  38. from pprint import pprint
  39. from select import select
  40. from shutil import get_terminal_size
  41. from textwrap import dedent
  42. from time import sleep
  43. from unicodedata import east_asian_width
  44. #########################################################################################
  45. # v bindings
  46. try:
  47. DLL = CDLL("libpulse.so.0")
  48. except Exception as e:
  49. sys.exit(e)
  50. PA_VOLUME_NORM = 65536
  51. PA_CHANNELS_MAX = 32
  52. PA_USEC_T = c_uint64
  53. PA_CONTEXT_READY = 4
  54. PA_CONTEXT_FAILED = 5
  55. PA_SUBSCRIPTION_MASK_ALL = 0x02ff
  56. class Struct(Structure): pass
  57. PA_PROPLIST = PA_OPERATION = PA_CONTEXT = PA_THREADED_MAINLOOP = PA_MAINLOOP_API = Struct
  58. class PA_SAMPLE_SPEC(Structure):
  59. _fields_ = [
  60. ("format", c_int),
  61. ("rate", c_uint32),
  62. ("channels", c_uint32)
  63. ]
  64. class PA_CHANNEL_MAP(Structure):
  65. _fields_ = [
  66. ("channels", c_uint8),
  67. ("map", c_int * PA_CHANNELS_MAX)
  68. ]
  69. class PA_CVOLUME(Structure):
  70. _fields_ = [
  71. ("channels", c_uint8),
  72. ("values", c_uint32 * PA_CHANNELS_MAX)
  73. ]
  74. class PA_PORT_INFO(Structure):
  75. _fields_ = [
  76. ('name', c_char_p),
  77. ('description', c_char_p),
  78. ('priority', c_uint32),
  79. ("available", c_int),
  80. ]
  81. class PA_SINK_INPUT_INFO(Structure):
  82. _fields_ = [
  83. ("index", c_uint32),
  84. ("name", c_char_p),
  85. ("owner_module", c_uint32),
  86. ("client", c_uint32),
  87. ("sink", c_uint32),
  88. ("sample_spec", PA_SAMPLE_SPEC),
  89. ("channel_map", PA_CHANNEL_MAP),
  90. ("volume", PA_CVOLUME),
  91. ("buffer_usec", PA_USEC_T),
  92. ("sink_usec", PA_USEC_T),
  93. ("resample_method", c_char_p),
  94. ("driver", c_char_p),
  95. ("mute", c_int),
  96. ("proplist", POINTER(PA_PROPLIST))
  97. ]
  98. class PA_SINK_INFO(Structure):
  99. _fields_ = [
  100. ("name", c_char_p),
  101. ("index", c_uint32),
  102. ("description", c_char_p),
  103. ("sample_spec", PA_SAMPLE_SPEC),
  104. ("channel_map", PA_CHANNEL_MAP),
  105. ("owner_module", c_uint32),
  106. ("volume", PA_CVOLUME),
  107. ("mute", c_int),
  108. ("monitor_source", c_uint32),
  109. ("monitor_source_name", c_char_p),
  110. ("latency", PA_USEC_T),
  111. ("driver", c_char_p),
  112. ("flags", c_int),
  113. ("proplist", POINTER(PA_PROPLIST)),
  114. ("configured_latency", PA_USEC_T),
  115. ('base_volume', c_int),
  116. ('state', c_int),
  117. ('n_volume_steps', c_int),
  118. ('card', c_uint32),
  119. ('n_ports', c_uint32),
  120. ('ports', POINTER(POINTER(PA_PORT_INFO))),
  121. ('active_port', POINTER(PA_PORT_INFO))
  122. ]
  123. class PA_SOURCE_OUTPUT_INFO(Structure):
  124. _fields_ = [
  125. ("index", c_uint32),
  126. ("name", c_char_p),
  127. ("owner_module", c_uint32),
  128. ("client", c_uint32),
  129. ("source", c_uint32),
  130. ("sample_spec", PA_SAMPLE_SPEC),
  131. ("channel_map", PA_CHANNEL_MAP),
  132. ("buffer_usec", PA_USEC_T),
  133. ("source_usec", PA_USEC_T),
  134. ("resample_method", c_char_p),
  135. ("driver", c_char_p),
  136. ("proplist", POINTER(PA_PROPLIST)),
  137. ("corked", c_int),
  138. ("volume", PA_CVOLUME),
  139. ("mute", c_int),
  140. ]
  141. class PA_SOURCE_INFO(Structure):
  142. _fields_ = [
  143. ("name", c_char_p),
  144. ("index", c_uint32),
  145. ("description", c_char_p),
  146. ("sample_spec", PA_SAMPLE_SPEC),
  147. ("channel_map", PA_CHANNEL_MAP),
  148. ("owner_module", c_uint32),
  149. ("volume", PA_CVOLUME),
  150. ("mute", c_int),
  151. ("monitor_of_sink", c_uint32),
  152. ("monitor_of_sink_name", c_char_p),
  153. ("latency", PA_USEC_T),
  154. ("driver", c_char_p),
  155. ("flags", c_int),
  156. ("proplist", POINTER(PA_PROPLIST)),
  157. ("configured_latency", PA_USEC_T),
  158. ('base_volume', c_int),
  159. ('state', c_int),
  160. ('n_volume_steps', c_int),
  161. ('card', c_uint32),
  162. ('n_ports', c_uint32),
  163. ('ports', POINTER(POINTER(PA_PORT_INFO))),
  164. ('active_port', POINTER(PA_PORT_INFO))
  165. ]
  166. class PA_CLIENT_INFO(Structure):
  167. _fields_ = [
  168. ("index", c_uint32),
  169. ("name", c_char_p),
  170. ("owner_module", c_uint32),
  171. ("driver", c_char_p)
  172. ]
  173. class PA_CARD_PROFILE_INFO(Structure):
  174. _fields_ = [
  175. ('name', c_char_p),
  176. ('description', c_char_p),
  177. ('n_sinks', c_uint32),
  178. ('n_sources', c_uint32),
  179. ('priority', c_uint32),
  180. ]
  181. class PA_CARD_PROFILE_INFO2(Structure):
  182. _fields_ = PA_CARD_PROFILE_INFO._fields_ + [('available', c_int)]
  183. class PA_CARD_INFO(Structure):
  184. _fields_ = [
  185. ('index', c_uint32),
  186. ('name', c_char_p),
  187. ('owner_module', c_uint32),
  188. ('driver', c_char_p),
  189. ('n_profiles', c_uint32),
  190. ('profiles', POINTER(PA_CARD_PROFILE_INFO)),
  191. ('active_profile', POINTER(PA_CARD_PROFILE_INFO)),
  192. ('proplist', POINTER(PA_PROPLIST)),
  193. ('n_ports', c_uint32),
  194. ('ports', POINTER(POINTER(c_void_p))),
  195. ('profiles2', POINTER(POINTER(PA_CARD_PROFILE_INFO2))),
  196. ('active_profile2', POINTER(PA_CARD_PROFILE_INFO2))
  197. ]
  198. class PA_SERVER_INFO(Structure):
  199. _fields_ = [
  200. ('user_name', c_char_p),
  201. ('host_name', c_char_p),
  202. ('server_version', c_char_p),
  203. ('server_name', c_char_p),
  204. ('sample_spec', PA_SAMPLE_SPEC),
  205. ('default_sink_name', c_char_p),
  206. ('default_source_name', c_char_p),
  207. ]
  208. PA_STATE_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), c_void_p)
  209. PA_CLIENT_INFO_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), POINTER(PA_CLIENT_INFO), c_int, c_void_p)
  210. PA_SINK_INPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INPUT_INFO), c_int, c_void_p)
  211. PA_SINK_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SINK_INFO), c_int, c_void_p)
  212. PA_SOURCE_OUTPUT_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_OUTPUT_INFO), c_int, c_void_p)
  213. PA_SOURCE_INFO_CB_T = CFUNCTYPE(c_int, POINTER(PA_CONTEXT), POINTER(PA_SOURCE_INFO), c_int, c_void_p)
  214. PA_CONTEXT_SUCCESS_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), c_int, c_void_p)
  215. PA_CARD_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_CARD_INFO), c_int, c_void_p)
  216. PA_SERVER_INFO_CB_T = CFUNCTYPE(None, POINTER(PA_CONTEXT), POINTER(PA_SERVER_INFO), c_void_p)
  217. PA_CONTEXT_SUBSCRIBE_CB_T = CFUNCTYPE(c_void_p, POINTER(PA_CONTEXT), c_int, c_int, c_void_p)
  218. pa_threaded_mainloop_new = DLL.pa_threaded_mainloop_new
  219. pa_threaded_mainloop_new.restype = POINTER(PA_THREADED_MAINLOOP)
  220. pa_threaded_mainloop_new.argtypes = []
  221. pa_threaded_mainloop_free = DLL.pa_threaded_mainloop_free
  222. pa_threaded_mainloop_free.restype = c_void_p
  223. pa_threaded_mainloop_free.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  224. pa_threaded_mainloop_start = DLL.pa_threaded_mainloop_start
  225. pa_threaded_mainloop_start.restype = c_int
  226. pa_threaded_mainloop_start.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  227. pa_threaded_mainloop_stop = DLL.pa_threaded_mainloop_stop
  228. pa_threaded_mainloop_stop.restype = None
  229. pa_threaded_mainloop_stop.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  230. pa_threaded_mainloop_lock = DLL.pa_threaded_mainloop_lock
  231. pa_threaded_mainloop_lock.restype = None
  232. pa_threaded_mainloop_lock.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  233. pa_threaded_mainloop_unlock = DLL.pa_threaded_mainloop_unlock
  234. pa_threaded_mainloop_unlock.restype = None
  235. pa_threaded_mainloop_unlock.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  236. pa_threaded_mainloop_wait = DLL.pa_threaded_mainloop_wait
  237. pa_threaded_mainloop_wait.restype = None
  238. pa_threaded_mainloop_wait.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  239. pa_threaded_mainloop_signal = DLL.pa_threaded_mainloop_signal
  240. pa_threaded_mainloop_signal.restype = None
  241. pa_threaded_mainloop_signal.argtypes = [POINTER(PA_THREADED_MAINLOOP), c_int]
  242. pa_threaded_mainloop_get_api = DLL.pa_threaded_mainloop_get_api
  243. pa_threaded_mainloop_get_api.restype = POINTER(PA_MAINLOOP_API)
  244. pa_threaded_mainloop_get_api.argtypes = [POINTER(PA_THREADED_MAINLOOP)]
  245. pa_context_errno = DLL.pa_context_errno
  246. pa_context_errno.restype = c_int
  247. pa_context_errno.argtypes = [POINTER(PA_CONTEXT)]
  248. pa_context_new_with_proplist = DLL.pa_context_new_with_proplist
  249. pa_context_new_with_proplist.restype = POINTER(PA_CONTEXT)
  250. pa_context_new_with_proplist.argtypes = [POINTER(PA_MAINLOOP_API), c_char_p, POINTER(PA_PROPLIST)]
  251. pa_context_unref = DLL.pa_context_unref
  252. pa_context_unref.restype = None
  253. pa_context_unref.argtypes = [POINTER(PA_CONTEXT)]
  254. pa_context_set_state_callback = DLL.pa_context_set_state_callback
  255. pa_context_set_state_callback.restype = None
  256. pa_context_set_state_callback.argtypes = [POINTER(PA_CONTEXT), PA_STATE_CB_T, c_void_p]
  257. pa_context_connect = DLL.pa_context_connect
  258. pa_context_connect.restype = c_int
  259. pa_context_connect.argtypes = [POINTER(PA_CONTEXT), c_char_p, c_int, POINTER(c_int)]
  260. pa_context_get_state = DLL.pa_context_get_state
  261. pa_context_get_state.restype = c_int
  262. pa_context_get_state.argtypes = [POINTER(PA_CONTEXT)]
  263. pa_context_disconnect = DLL.pa_context_disconnect
  264. pa_context_disconnect.restype = c_int
  265. pa_context_disconnect.argtypes = [POINTER(PA_CONTEXT)]
  266. pa_operation_unref = DLL.pa_operation_unref
  267. pa_operation_unref.restype = None
  268. pa_operation_unref.argtypes = [POINTER(PA_OPERATION)]
  269. pa_context_subscribe = DLL.pa_context_subscribe
  270. pa_context_subscribe.restype = POINTER(PA_OPERATION)
  271. pa_context_subscribe.argtypes = [POINTER(PA_CONTEXT), c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  272. pa_context_set_subscribe_callback = DLL.pa_context_set_subscribe_callback
  273. pa_context_set_subscribe_callback.restype = None
  274. pa_context_set_subscribe_callback.args = [POINTER(PA_CONTEXT), PA_CONTEXT_SUBSCRIBE_CB_T, c_void_p]
  275. pa_proplist_new = DLL.pa_proplist_new
  276. pa_proplist_new.restype = POINTER(PA_PROPLIST)
  277. pa_proplist_sets = DLL.pa_proplist_sets
  278. pa_proplist_sets.argtypes = [POINTER(PA_PROPLIST), c_char_p, c_char_p]
  279. pa_proplist_gets = DLL.pa_proplist_gets
  280. pa_proplist_gets.restype = c_char_p
  281. pa_proplist_gets.argtypes = [POINTER(PA_PROPLIST), c_char_p]
  282. pa_proplist_free = DLL.pa_proplist_free
  283. pa_proplist_free.argtypes = [POINTER(PA_PROPLIST)]
  284. pa_context_get_sink_input_info_list = DLL.pa_context_get_sink_input_info_list
  285. pa_context_get_sink_input_info_list.restype = POINTER(PA_OPERATION)
  286. pa_context_get_sink_input_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SINK_INPUT_INFO_CB_T, c_void_p]
  287. pa_context_get_sink_info_list = DLL.pa_context_get_sink_info_list
  288. pa_context_get_sink_info_list.restype = POINTER(PA_OPERATION)
  289. pa_context_get_sink_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SINK_INFO_CB_T, c_void_p]
  290. pa_context_set_sink_mute_by_index = DLL.pa_context_set_sink_mute_by_index
  291. pa_context_set_sink_mute_by_index.restype = POINTER(PA_OPERATION)
  292. pa_context_set_sink_mute_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  293. pa_context_suspend_sink_by_index = DLL.pa_context_suspend_sink_by_index
  294. pa_context_suspend_sink_by_index.restype = POINTER(PA_OPERATION)
  295. pa_context_suspend_sink_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  296. pa_context_set_sink_port_by_index = DLL.pa_context_set_sink_port_by_index
  297. pa_context_set_sink_port_by_index.restype = POINTER(PA_OPERATION)
  298. pa_context_set_sink_port_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  299. pa_context_set_sink_input_mute = DLL.pa_context_set_sink_input_mute
  300. pa_context_set_sink_input_mute.restype = POINTER(PA_OPERATION)
  301. pa_context_set_sink_input_mute.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  302. pa_context_set_sink_volume_by_index = DLL.pa_context_set_sink_volume_by_index
  303. pa_context_set_sink_volume_by_index.restype = POINTER(PA_OPERATION)
  304. pa_context_set_sink_volume_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  305. pa_context_set_sink_input_volume = DLL.pa_context_set_sink_input_volume
  306. pa_context_set_sink_input_volume.restype = POINTER(PA_OPERATION)
  307. pa_context_set_sink_input_volume.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  308. pa_context_move_sink_input_by_index = DLL.pa_context_move_sink_input_by_index
  309. pa_context_move_sink_input_by_index.restype = POINTER(PA_OPERATION)
  310. pa_context_move_sink_input_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  311. pa_context_set_default_sink = DLL.pa_context_set_default_sink
  312. pa_context_set_default_sink.restype = POINTER(PA_OPERATION)
  313. pa_context_set_default_sink.argtypes = [POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  314. pa_context_kill_sink_input = DLL.pa_context_kill_sink_input
  315. pa_context_kill_sink_input.restype = POINTER(PA_OPERATION)
  316. pa_context_kill_sink_input.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  317. pa_context_kill_client = DLL.pa_context_kill_client
  318. pa_context_kill_client.restype = POINTER(PA_OPERATION)
  319. pa_context_kill_client.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  320. pa_context_get_source_output_info_list = DLL.pa_context_get_source_output_info_list
  321. pa_context_get_source_output_info_list.restype = POINTER(PA_OPERATION)
  322. pa_context_get_source_output_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SOURCE_OUTPUT_INFO_CB_T, c_void_p]
  323. pa_context_move_source_output_by_index = DLL.pa_context_move_source_output_by_index
  324. pa_context_move_source_output_by_index.restype = POINTER(PA_OPERATION)
  325. pa_context_move_source_output_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  326. pa_context_set_source_output_volume = DLL.pa_context_set_source_output_volume
  327. pa_context_set_source_output_volume.restype = POINTER(PA_OPERATION)
  328. pa_context_set_source_output_volume.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  329. pa_context_set_source_output_mute = DLL.pa_context_set_source_output_mute
  330. pa_context_set_source_output_mute.restype = POINTER(PA_OPERATION)
  331. pa_context_set_source_output_mute.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  332. pa_context_get_source_info_list = DLL.pa_context_get_source_info_list
  333. pa_context_get_source_info_list.restype = POINTER(PA_OPERATION)
  334. pa_context_get_source_info_list.argtypes = [POINTER(PA_CONTEXT), PA_SOURCE_INFO_CB_T, c_void_p]
  335. pa_context_set_source_volume_by_index = DLL.pa_context_set_source_volume_by_index
  336. pa_context_set_source_volume_by_index.restype = POINTER(PA_OPERATION)
  337. pa_context_set_source_volume_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, POINTER(PA_CVOLUME), PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  338. pa_context_set_source_mute_by_index = DLL.pa_context_set_source_mute_by_index
  339. pa_context_set_source_mute_by_index.restype = POINTER(PA_OPERATION)
  340. pa_context_set_source_mute_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  341. pa_context_suspend_source_by_index = DLL.pa_context_suspend_source_by_index
  342. pa_context_suspend_source_by_index.restype = POINTER(PA_OPERATION)
  343. pa_context_suspend_source_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_int, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  344. pa_context_set_source_port_by_index = DLL.pa_context_set_source_port_by_index
  345. pa_context_set_source_port_by_index.restype = POINTER(PA_OPERATION)
  346. pa_context_set_source_port_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  347. pa_context_set_default_source = DLL.pa_context_set_default_source
  348. pa_context_set_default_source.restype = POINTER(PA_OPERATION)
  349. pa_context_set_default_source.argtypes = [POINTER(PA_CONTEXT), c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  350. pa_context_kill_source_output = DLL.pa_context_kill_source_output
  351. pa_context_kill_source_output.restype = POINTER(PA_OPERATION)
  352. pa_context_kill_source_output.argtypes = [POINTER(PA_CONTEXT), c_uint32, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  353. pa_context_get_client_info_list = DLL.pa_context_get_client_info_list
  354. pa_context_get_client_info_list.restype = POINTER(PA_OPERATION)
  355. pa_context_get_client_info_list.argtypes = [POINTER(PA_CONTEXT), PA_CLIENT_INFO_CB_T, c_void_p]
  356. pa_context_get_card_info_list = DLL.pa_context_get_card_info_list
  357. pa_context_get_card_info_list.restype = POINTER(PA_OPERATION)
  358. pa_context_get_card_info_list.argtypes = [POINTER(PA_CONTEXT), PA_CARD_INFO_CB_T, c_void_p]
  359. pa_context_set_card_profile_by_index = DLL.pa_context_set_card_profile_by_index
  360. pa_context_set_card_profile_by_index.restype = POINTER(PA_OPERATION)
  361. pa_context_set_card_profile_by_index.argtypes = [POINTER(PA_CONTEXT), c_uint32, c_char_p, PA_CONTEXT_SUCCESS_CB_T, c_void_p]
  362. pa_context_get_server_info = DLL.pa_context_get_server_info
  363. pa_context_get_server_info.restype = POINTER(PA_OPERATION)
  364. pa_context_get_server_info.argtypes = [POINTER(PA_CONTEXT), PA_SERVER_INFO_CB_T, c_void_p]
  365. pa_get_library_version = DLL.pa_get_library_version
  366. pa_get_library_version.restype = c_char_p
  367. PA_MAJOR = int(pa_get_library_version().decode().split('.')[0])
  368. # ^ bindings
  369. #########################################################################################
  370. # v lib
  371. class DebugMixin():
  372. def debug(self):
  373. pprint(vars(self))
  374. class PulsePort(DebugMixin):
  375. def __init__(self, pa_port):
  376. self.name = pa_port.name
  377. self.description = pa_port.description
  378. self.priority = pa_port.priority
  379. self.available = getattr(pa_port, "available", 0)
  380. if self.available == 1: # 1 off, 0 n/a, 2 on
  381. self.description += b' / off'
  382. class PulseServer(DebugMixin):
  383. def __init__(self, pa_server):
  384. self.default_sink_name = pa_server.default_sink_name
  385. self.default_source_name = pa_server.default_source_name
  386. self.server_version = pa_server.server_version
  387. class PulseCardProfile(DebugMixin):
  388. def __init__(self, pa_profile):
  389. self.name = pa_profile.name
  390. self.description = pa_profile.description
  391. self.available = getattr(pa_profile, "available", 1)
  392. if not self.available:
  393. self.description += b' / off'
  394. class PulseCard(DebugMixin):
  395. def __init__(self, pa_card):
  396. self.name = pa_card.name
  397. self.description = pa_proplist_gets(pa_card.proplist, b'device.description')
  398. self.index = pa_card.index
  399. self.driver = pa_card.driver
  400. self.owner_module = pa_card.owner_module
  401. self.n_profiles = pa_card.n_profiles
  402. if PA_MAJOR >= 5:
  403. self.profiles = [PulseCardProfile(pa_card.profiles2[n].contents) for n in range(self.n_profiles)]
  404. self.active_profile = PulseCardProfile(pa_card.active_profile2[0])
  405. else: # fallback to legacy profile, for PA < 5.0 (March 2014)
  406. self.profiles = [PulseCardProfile(pa_card.profiles[n]) for n in range(self.n_profiles)]
  407. self.active_profile = PulseCardProfile(pa_card.active_profile[0])
  408. self.volume = type('volume', (object, ), {'channels': 1, 'values': [0, 0]})
  409. def __str__(self):
  410. return "Card-ID: {}, Name: {}".format(self.index, self.name.decode())
  411. class PulseClient(DebugMixin):
  412. def __init__(self, pa_client):
  413. self.index = getattr(pa_client, "index", 0)
  414. self.name = getattr(pa_client, "name", pa_client)
  415. self.driver = getattr(pa_client, "driver", "default driver")
  416. self.owner_module = getattr(pa_client, "owner_module", -1)
  417. def __str__(self):
  418. return "Client-name: {}".format(self.name.decode())
  419. class Pulse(DebugMixin):
  420. def __init__(self, client_name='libpulse', server_name=None, reconnect=False):
  421. self.error = None
  422. self.data = []
  423. self.operation = None
  424. self.connected = False
  425. self.client_name = client_name.encode()
  426. self.server_name = server_name
  427. self.pa_state_cb = PA_STATE_CB_T(self.state_cb)
  428. self.pa_subscribe_cb = self.pa_dc_cb = lambda: None
  429. self.pa_cbs = {'sink_input_list': PA_SINK_INPUT_INFO_CB_T(self.sink_input_list_cb),
  430. 'source_output_list': PA_SOURCE_OUTPUT_INFO_CB_T(self.source_output_list_cb),
  431. 'sink_list': PA_SINK_INFO_CB_T(self.sink_list_cb),
  432. 'source_list': PA_SOURCE_INFO_CB_T(self.source_list_cb),
  433. 'server': PA_SERVER_INFO_CB_T(self.server_cb),
  434. 'card_list': PA_CARD_INFO_CB_T(self.card_list_cb),
  435. 'client_list': PA_CLIENT_INFO_CB_T(self.client_list_cb),
  436. 'success': PA_CONTEXT_SUCCESS_CB_T(self.context_success)}
  437. self.mainloop = pa_threaded_mainloop_new()
  438. self.mainloop_api = pa_threaded_mainloop_get_api(self.mainloop)
  439. proplist = pa_proplist_new()
  440. pa_proplist_sets(proplist, b'application.id', self.client_name)
  441. pa_proplist_sets(proplist, b'application.icon_name', b'audio-card')
  442. self.context = pa_context_new_with_proplist(self.mainloop_api, self.client_name, proplist)
  443. pa_context_set_state_callback(self.context, self.pa_state_cb, None)
  444. pa_proplist_free(proplist)
  445. if pa_context_connect(self.context, self.server_name, 0, None) < 0 or self.error:
  446. if not reconnect: sys.exit("Failed to connect to pulseaudio: Connection refused")
  447. else: return
  448. pa_threaded_mainloop_lock(self.mainloop)
  449. pa_threaded_mainloop_start(self.mainloop)
  450. if self.error and reconnect: return
  451. pa_threaded_mainloop_wait(self.mainloop) or pa_threaded_mainloop_unlock(self.mainloop)
  452. if self.error and reconnect: return
  453. elif self.error: sys.exit('Failed to connect to pulseaudio')
  454. self.connected = True
  455. def wait_and_unlock(self):
  456. pa_threaded_mainloop_wait(self.mainloop)
  457. pa_threaded_mainloop_unlock(self.mainloop)
  458. pa_operation_unref(self.operation)
  459. def reconnect(self):
  460. if self.context:
  461. pa_context_disconnect(self.context)
  462. pa_context_unref(self.context)
  463. if self.mainloop:
  464. pa_threaded_mainloop_stop(self.mainloop)
  465. pa_threaded_mainloop_free(self.mainloop)
  466. self.__init__(self.client_name.decode(), self.server_name, reconnect=True)
  467. def unmute_stream(self, obj):
  468. if type(obj) is PulseSinkInfo:
  469. self.sink_mute(obj.index, 0)
  470. elif type(obj) is PulseSinkInputInfo:
  471. self.sink_input_mute(obj.index, 0)
  472. elif type(obj) is PulseSourceInfo:
  473. self.source_mute(obj.index, 0)
  474. elif type(obj) is PulseSourceOutputInfo:
  475. self.source_output_mute(obj.index, 0)
  476. obj.mute = 0
  477. def mute_stream(self, obj):
  478. if type(obj) is PulseSinkInfo:
  479. self.sink_mute(obj.index, 1)
  480. elif type(obj) is PulseSinkInputInfo:
  481. self.sink_input_mute(obj.index, 1)
  482. elif type(obj) is PulseSourceInfo:
  483. self.source_mute(obj.index, 1)
  484. elif type(obj) is PulseSourceOutputInfo:
  485. self.source_output_mute(obj.index, 1)
  486. obj.mute = 1
  487. def set_volume(self, obj, volume):
  488. if type(obj) is PulseSinkInfo:
  489. self.set_sink_volume(obj.index, volume)
  490. elif type(obj) is PulseSinkInputInfo:
  491. self.set_sink_input_volume(obj.index, volume)
  492. elif type(obj) is PulseSourceInfo:
  493. self.set_source_volume(obj.index, volume)
  494. elif type(obj) is PulseSourceOutputInfo:
  495. self.set_source_output_volume(obj.index, volume)
  496. obj.volume = volume
  497. def change_volume_mono(self, obj, inc):
  498. obj.volume.values = [v + inc for v in obj.volume.values]
  499. self.set_volume(obj, obj.volume)
  500. def get_volume_mono(self, obj):
  501. return int(sum(obj.volume.values) / len(obj.volume.values))
  502. def fill_clients(self):
  503. if not self.data:
  504. return None
  505. data, self.data = self.data, []
  506. clist = self.client_list()
  507. for d in data:
  508. for c in clist:
  509. if c.index == d.client_id:
  510. d.client = c
  511. break
  512. return data
  513. def state_cb(self, c, b):
  514. state = pa_context_get_state(c)
  515. if state == PA_CONTEXT_READY:
  516. pa_threaded_mainloop_signal(self.mainloop, 0)
  517. elif state == PA_CONTEXT_FAILED:
  518. self.error = RuntimeError("Failed to complete action: {}, {}".format(state, pa_context_errno(c)))
  519. self.connected = False
  520. pa_threaded_mainloop_signal(self.mainloop, 0)
  521. self.pa_dc_cb()
  522. return 0
  523. def _eof_cb(func):
  524. def wrapper(self, c, info, eof, *args):
  525. if eof:
  526. pa_threaded_mainloop_signal(self.mainloop, 0)
  527. return 0
  528. func(self, c, info, eof, *args)
  529. return 0
  530. return wrapper
  531. def _action_sync(func):
  532. def wrapper(self, *args):
  533. if self.error: raise self.error
  534. pa_threaded_mainloop_lock(self.mainloop)
  535. try:
  536. func(self, *args)
  537. except Exception as e:
  538. pa_threaded_mainloop_unlock(self.mainloop)
  539. raise e
  540. self.wait_and_unlock()
  541. if func.__name__ in ('sink_input_list', 'source_output_list'):
  542. self.data = self.fill_clients()
  543. data, self.data = self.data, []
  544. return data or []
  545. return wrapper
  546. @_eof_cb
  547. def card_list_cb(self, c, card_info, eof, userdata):
  548. self.data.append(PulseCard(card_info[0]))
  549. @_eof_cb
  550. def client_list_cb(self, c, client_info, eof, userdata):
  551. self.data.append(PulseClient(client_info[0]))
  552. @_eof_cb
  553. def sink_input_list_cb(self, c, sink_input_info, eof, userdata):
  554. self.data.append(PulseSinkInputInfo(sink_input_info[0]))
  555. @_eof_cb
  556. def sink_list_cb(self, c, sink_info, eof, userdata):
  557. self.data.append(PulseSinkInfo(sink_info[0]))
  558. @_eof_cb
  559. def source_output_list_cb(self, c, source_output_info, eof, userdata):
  560. self.data.append(PulseSourceOutputInfo(source_output_info[0]))
  561. @_eof_cb
  562. def source_list_cb(self, c, source_info, eof, userdata):
  563. self.data.append(PulseSourceInfo(source_info[0]))
  564. def server_cb(self, c, server_info, userdata):
  565. self.data = PulseServer(server_info[0])
  566. pa_threaded_mainloop_signal(self.mainloop, 0)
  567. def context_success(self, *_):
  568. pa_threaded_mainloop_signal(self.mainloop, 0)
  569. def subscribe(self, cb):
  570. self.pa_subscribe_cb, self.pa_dc_cb = PA_CONTEXT_SUBSCRIBE_CB_T(cb), cb
  571. pa_context_set_subscribe_callback(self.context, self.pa_subscribe_cb, None)
  572. pa_threaded_mainloop_lock(self.mainloop)
  573. self.operation = pa_context_subscribe(self.context, PA_SUBSCRIPTION_MASK_ALL, self.pa_cbs['success'], None)
  574. self.wait_and_unlock()
  575. @_action_sync
  576. def sink_input_list(self):
  577. self.operation = pa_context_get_sink_input_info_list(self.context, self.pa_cbs['sink_input_list'], None)
  578. @_action_sync
  579. def source_output_list(self):
  580. self.operation = pa_context_get_source_output_info_list(self.context, self.pa_cbs['source_output_list'], None)
  581. @_action_sync
  582. def sink_list(self):
  583. self.operation = pa_context_get_sink_info_list(self.context, self.pa_cbs['sink_list'], None)
  584. @_action_sync
  585. def source_list(self):
  586. self.operation = pa_context_get_source_info_list(self.context, self.pa_cbs['source_list'], None)
  587. @_action_sync
  588. def get_server_info(self):
  589. self.operation = pa_context_get_server_info(self.context, self.pa_cbs['server'], None)
  590. @_action_sync
  591. def card_list(self):
  592. self.operation = pa_context_get_card_info_list(self.context, self.pa_cbs['card_list'], None)
  593. @_action_sync
  594. def client_list(self):
  595. self.operation = pa_context_get_client_info_list(self.context, self.pa_cbs['client_list'], None)
  596. @_action_sync
  597. def sink_input_mute(self, index, mute):
  598. self.operation = pa_context_set_sink_input_mute(self.context, index, mute, self.pa_cbs['success'], None)
  599. @_action_sync
  600. def sink_input_move(self, index, s_index):
  601. self.operation = pa_context_move_sink_input_by_index(self.context, index, s_index, self.pa_cbs['success'], None)
  602. @_action_sync
  603. def sink_mute(self, index, mute):
  604. self.operation = pa_context_set_sink_mute_by_index(self.context, index, mute, self.pa_cbs['success'], None)
  605. @_action_sync
  606. def set_sink_input_volume(self, index, vol):
  607. self.operation = pa_context_set_sink_input_volume(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
  608. @_action_sync
  609. def set_sink_volume(self, index, vol):
  610. self.operation = pa_context_set_sink_volume_by_index(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
  611. @_action_sync
  612. def sink_suspend(self, index, suspend):
  613. self.operation = pa_context_suspend_sink_by_index(self.context, index, suspend, self.pa_cbs['success'], None)
  614. @_action_sync
  615. def set_default_sink(self, name):
  616. self.operation = pa_context_set_default_sink(self.context, name, self.pa_cbs['success'], None)
  617. @_action_sync
  618. def kill_sink(self, index):
  619. self.operation = pa_context_kill_sink_input(self.context, index, self.pa_cbs['success'], None)
  620. @_action_sync
  621. def kill_client(self, index):
  622. self.operation = pa_context_kill_client(self.context, index, self.pa_cbs['success'], None)
  623. @_action_sync
  624. def set_sink_port(self, index, port):
  625. self.operation = pa_context_set_sink_port_by_index(self.context, index, port, self.pa_cbs['success'], None)
  626. @_action_sync
  627. def set_source_output_volume(self, index, vol):
  628. self.operation = pa_context_set_source_output_volume(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
  629. @_action_sync
  630. def set_source_volume(self, index, vol):
  631. self.operation = pa_context_set_source_volume_by_index(self.context, index, vol.to_c(), self.pa_cbs['success'], None)
  632. @_action_sync
  633. def source_suspend(self, index, suspend):
  634. self.operation = pa_context_suspend_source_by_index(self.context, index, suspend, self.pa_cbs['success'], None)
  635. @_action_sync
  636. def set_default_source(self, name):
  637. self.operation = pa_context_set_default_source(self.context, name, self.pa_cbs['success'], None)
  638. @_action_sync
  639. def kill_source(self, index):
  640. self.operation = pa_context_kill_source_output(self.context, index, self.pa_cbs['success'], None)
  641. @_action_sync
  642. def set_source_port(self, index, port):
  643. self.operation = pa_context_set_source_port_by_index(self.context, index, port, self.pa_cbs['success'], None)
  644. @_action_sync
  645. def source_output_mute(self, index, mute):
  646. self.operation = pa_context_set_source_output_mute(self.context, index, mute, self.pa_cbs['success'], None)
  647. @_action_sync
  648. def source_mute(self, index, mute):
  649. self.operation = pa_context_set_source_mute_by_index(self.context, index, mute, self.pa_cbs['success'], None)
  650. @_action_sync
  651. def source_output_move(self, index, s_index):
  652. self.operation = pa_context_move_source_output_by_index(self.context, index, s_index, self.pa_cbs['success'], None)
  653. @_action_sync
  654. def set_card_profile(self, index, p_index):
  655. self.operation = pa_context_set_card_profile_by_index(self.context, index, p_index, self.pa_cbs['success'], None)
  656. class PulseSink(DebugMixin):
  657. def __init__(self, sink_info):
  658. self.index = sink_info.index
  659. self.name = sink_info.name
  660. self.mute = sink_info.mute
  661. self.volume = PulseVolume(sink_info.volume)
  662. class PulseSinkInfo(PulseSink):
  663. def __init__(self, pa_sink_info):
  664. PulseSink.__init__(self, pa_sink_info)
  665. self.description = pa_sink_info.description
  666. self.owner_module = pa_sink_info.owner_module
  667. self.driver = pa_sink_info.driver
  668. self.monitor_source = pa_sink_info.monitor_source
  669. self.monitor_source_name = pa_sink_info.monitor_source_name
  670. self.n_ports = pa_sink_info.n_ports
  671. self.ports = [PulsePort(pa_sink_info.ports[i].contents) for i in range(self.n_ports)]
  672. self.active_port = None
  673. if self.n_ports:
  674. self.active_port = PulsePort(pa_sink_info.active_port.contents)
  675. def __str__(self):
  676. return "ID: sink-{}, Name: {}, Mute: {}, {}".format(self.index, self.description.decode(), self.mute, self.volume)
  677. class PulseSinkInputInfo(PulseSink):
  678. def __init__(self, pa_sink_input_info):
  679. PulseSink.__init__(self, pa_sink_input_info)
  680. self.owner_module = pa_sink_input_info.owner_module
  681. self.client = PulseClient(pa_sink_input_info.name)
  682. self.client_id = pa_sink_input_info.client
  683. self.sink = self.owner = pa_sink_input_info.sink
  684. self.driver = pa_sink_input_info.driver
  685. self.media_name = pa_proplist_gets(pa_sink_input_info.proplist, b'media.name')
  686. def __str__(self):
  687. if self.client:
  688. return "ID: sink-input-{}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
  689. return "ID: sink-input-{}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute)
  690. class PulseSource(DebugMixin):
  691. def __init__(self, source_info):
  692. self.index = source_info.index
  693. self.name = source_info.name
  694. self.mute = source_info.mute
  695. self.volume = PulseVolume(source_info.volume)
  696. class PulseSourceInfo(PulseSource):
  697. def __init__(self, pa_source_info):
  698. PulseSource.__init__(self, pa_source_info)
  699. self.description = pa_source_info.description
  700. self.owner_module = pa_source_info.owner_module
  701. self.monitor_of_sink = pa_source_info.monitor_of_sink
  702. self.monitor_of_sink_name = pa_source_info.monitor_of_sink_name
  703. self.driver = pa_source_info.driver
  704. self.n_ports = pa_source_info.n_ports
  705. self.ports = [PulsePort(pa_source_info.ports[i].contents) for i in range(self.n_ports)]
  706. self.active_port = None
  707. if self.n_ports:
  708. self.active_port = PulsePort(pa_source_info.active_port.contents)
  709. def __str__(self):
  710. return "ID: source-{}, Name: {}, Mute: {}, {}".format(self.index, self.description.decode(), self.mute, self.volume)
  711. class PulseSourceOutputInfo(PulseSource):
  712. def __init__(self, pa_source_output_info):
  713. PulseSource.__init__(self, pa_source_output_info)
  714. self.owner_module = pa_source_output_info.owner_module
  715. self.client = PulseClient(pa_source_output_info.name)
  716. self.client_id = pa_source_output_info.client
  717. self.source = self.owner = pa_source_output_info.source
  718. self.driver = pa_source_output_info.driver
  719. self.application_id = pa_proplist_gets(pa_source_output_info.proplist, b'application.id')
  720. def __str__(self):
  721. if self.client:
  722. return "ID: source-output-{}, Name: {}, Mute: {}, {}".format(self.index, self.client.name.decode(), self.mute, self.volume)
  723. return "ID: source-output-{}, Name: {}, Mute: {}".format(self.index, self.name.decode(), self.mute)
  724. class PulseVolume(DebugMixin):
  725. def __init__(self, cvolume):
  726. self.channels = cvolume.channels
  727. self.values = [(round(x * 100 / PA_VOLUME_NORM)) for x in cvolume.values[:self.channels]]
  728. self.cvolume = PA_CVOLUME()
  729. self.cvolume.channels = self.channels
  730. def to_c(self):
  731. self.values = list(map(lambda x: max(min(x, 150), 0), self.values))
  732. for x in range(self.channels):
  733. self.cvolume.values[x] = round((self.values[x] * PA_VOLUME_NORM) / 100)
  734. return self.cvolume
  735. def __str__(self):
  736. return "Channels: {}, Volumes: {}".format(self.channels, [str(x) + "%" for x in self.values])
  737. # ^ lib
  738. #########################################################################################
  739. # v main
  740. class Bar():
  741. # should be in correct order
  742. LEFT, RIGHT, RLEFT, RRIGHT, CENTER, SUB, SLEFT, SRIGHT, NONE = range(9)
  743. def __init__(self, pa):
  744. if type(pa) is str:
  745. self.name = pa
  746. return
  747. if type(pa) in (PulseSinkInfo, PulseSourceInfo, PulseCard):
  748. self.fullname = pa.description.decode()
  749. else:
  750. self.fullname = pa.client.name.decode()
  751. self.name = re.sub(r'^ALSA plug-in \[|\]$', '', self.fullname.replace('|', ' '))
  752. for key in CFG.renames:
  753. if key.match(self.name):
  754. self.name = CFG.renames[key]
  755. break
  756. self.index = pa.index
  757. self.owner = -1
  758. self.stream_index = -1
  759. self.media_name, self.media_name_wide, self.media_name_widths = '', False, []
  760. self.poll_data(pa, 0, 0)
  761. self.maxsize = 150
  762. self.locked = True
  763. def poll_data(self, pa, owned, stream_index):
  764. self.channels = pa.volume.channels
  765. self.muted = getattr(pa, 'mute', False)
  766. self.owned = owned
  767. self.stream_index = stream_index
  768. self.volume = pa.volume.values
  769. if hasattr(pa, 'media_name'):
  770. media_fullname = pa.media_name.decode().replace('\n', ' ')
  771. media_name = ': {}'.format(media_fullname.replace('|', ' '))
  772. if media_fullname != self.fullname and media_name != self.media_name:
  773. self.media_name, self.media_name_wide = media_name, False
  774. if len(media_fullname) != len(pa.media_name): # contains multi-byte chars which might be wide
  775. self.media_name_widths = [int(east_asian_width(c) == 'W') + 1 for c in media_name]
  776. self.media_name_wide = 2 in self.media_name_widths
  777. else:
  778. self.media_name, self.media_name_wide = '', False
  779. if type(pa) in (PulseSinkInputInfo, PulseSourceOutputInfo):
  780. self.owner = pa.owner
  781. self.pa = pa
  782. def mute_toggle(self):
  783. PULSE.unmute_stream(self.pa) if self.muted else PULSE.mute_stream(self.pa)
  784. def lock_toggle(self):
  785. self.locked = not self.locked
  786. def set(self, n, side):
  787. vol = self.pa.volume
  788. if self.locked:
  789. for i, _ in enumerate(vol.values):
  790. vol.values[i] = n
  791. else:
  792. vol.values[side] = n
  793. PULSE.set_volume(self.pa, vol)
  794. def move(self, n, side):
  795. vol = self.pa.volume
  796. if self.locked:
  797. for i, _ in enumerate(vol.values):
  798. vol.values[i] += n
  799. else:
  800. vol.values[side] += n
  801. PULSE.set_volume(self.pa, vol)
  802. class Screen():
  803. DOWN = 1
  804. UP = -1
  805. SCROLL_UP = [getattr(curses, i, 0) for i in ['BUTTON4_PRESSED', 'BUTTON3_TRIPLE_CLICKED']]
  806. SCROLL_DOWN = [getattr(curses, i, 0) for i in ['BUTTON5_PRESSED', 'A_LOW', 'A_BOLD', 'BUTTON4_DOUBLE_CLICKED']]
  807. KEY_MOUSE = getattr(curses, 'KEY_MOUSE', 0)
  808. DIGITS = list(map(ord, map(str, range(10))))
  809. SIDES = {Bar.LEFT: 'Left', Bar.RIGHT: 'Right', Bar.RLEFT: 'Rear Left',
  810. Bar.RRIGHT: 'Rear Right', Bar.CENTER: 'Center', Bar.SUB: 'Subwoofer',
  811. Bar.SLEFT: 'Side left', Bar.SRIGHT: 'Side right'}
  812. SEQ_TO_KEY = {159: curses.KEY_F1, 160: curses.KEY_F2, 161: curses.KEY_F3,
  813. 316: curses.KEY_SRIGHT, 317: curses.KEY_SLEFT,
  814. 151: curses.KEY_HOME, 266: curses.KEY_HOME,
  815. 149: curses.KEY_END, 269: curses.KEY_END}
  816. def __init__(self, color=2, mouse=True):
  817. os.environ['ESCDELAY'] = '25'
  818. self.screen = curses.initscr()
  819. self.screen.nodelay(True)
  820. self.screen.scrollok(1)
  821. if mouse:
  822. try:
  823. curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.BUTTON1_CLICKED | self.KEY_MOUSE |
  824. functools.reduce(operator.or_, list(self.SCROLL_UP)) |
  825. functools.reduce(operator.or_, list(self.SCROLL_DOWN)))
  826. except:
  827. self.KEY_MOUSE = 0
  828. else:
  829. self.KEY_MOUSE = 0
  830. try:
  831. curses.curs_set(0)
  832. except: # terminal doesn't support visibility requests
  833. pass
  834. self.screen.border(0)
  835. self.screen.clear()
  836. self.screen.refresh()
  837. self.index = 0
  838. self.top_line_num = 0
  839. self.focus_line_num = 0
  840. self.lines, self.cols = curses.LINES - 2, curses.COLS - 1
  841. self.info, self.menu = str, str
  842. self.mode_keys = self.get_mode_keys()
  843. self.menu_titles = ['{} Output'.format(self.mode_keys[0]),
  844. '{} Input'.format(self.mode_keys[1]),
  845. '{} Cards'.format(self.mode_keys[2])]
  846. self.data = []
  847. self.mode = {0: 1, 1: 0, 2: 0}
  848. self.modes_data = [[[], 0, 0] for i in range(6)]
  849. self.active_mode = 0
  850. self.old_mode = 0
  851. self.change_mode_allowed = True
  852. self.n_lines = 0
  853. self.color_mode = color
  854. if color in (1, 2) and curses.has_colors():
  855. curses.start_color()
  856. curses.use_default_colors()
  857. curses.init_pair(1, curses.COLOR_GREEN, -1)
  858. curses.init_pair(2, curses.COLOR_YELLOW, -1)
  859. curses.init_pair(3, curses.COLOR_RED, -1)
  860. self.green = curses.color_pair(1)
  861. self.yellow = curses.color_pair(2)
  862. self.red = curses.color_pair(3)
  863. n = 7 if curses.COLORS < 256 else 67
  864. curses.init_pair(n, n - 1, -1)
  865. self.muted_color = curses.color_pair(n)
  866. if curses.COLORS < 256:
  867. self.gray_gradient = [curses.A_NORMAL] * 3
  868. else:
  869. try:
  870. curses.init_pair(240, 240, -1)
  871. curses.init_pair(243, 243, -1)
  872. curses.init_pair(246, 246, -1)
  873. self.gray_gradient = [curses.color_pair(240),
  874. curses.color_pair(243),
  875. curses.color_pair(246)]
  876. except:
  877. self.gray_gradient = [curses.A_NORMAL] * 3
  878. else:
  879. # if term has colors start them regardless of --color to avoid weird backgrounds on some terminals
  880. if curses.has_colors():
  881. curses.start_color()
  882. curses.use_default_colors()
  883. self.gray_gradient = [curses.A_NORMAL] * 3
  884. self.green = self.yellow = self.red = self.muted_color = curses.A_NORMAL
  885. self.gradient = [self.green, self.yellow, self.red]
  886. self.submenu_data = []
  887. self.submenu_width = 30
  888. self.submenu_show = False
  889. self.submenu = curses.newwin(curses.LINES, 0, 0, 0)
  890. self.helpwin_show = False
  891. self.helpwin = curses.newwin(14, 50, 0, 0)
  892. try:
  893. self.helpwin.mvwin((curses.LINES // 2) - 7, (curses.COLS // 2) - 25)
  894. except:
  895. pass
  896. self.selected = None
  897. self.action = None
  898. self.server_info = None
  899. self.ev = threading.Event()
  900. def getch(self):
  901. # blocking getch, can be 'interrupted' by ev.set
  902. self.ev.wait()
  903. self.ev.clear()
  904. c = self.screen.getch()
  905. if c == 27: # collect escape sequences as a single key
  906. seq_sum = sum(takewhile(lambda x: x != -1, [self.screen.getch() for _ in range(5)]))
  907. c = self.SEQ_TO_KEY.get(seq_sum, seq_sum + 128 if seq_sum else 27)
  908. return c
  909. def pregetcher(self):
  910. # because curses.getch doesn't work well with threads
  911. while True:
  912. select([sys.stdin], [], [], 10)
  913. self.ev.set()
  914. def wake_cb(self, *_):
  915. self.ev.set()
  916. def display_line(self, index, line, mod=curses.A_NORMAL, win=None):
  917. shift, win = 0, win or self.screen
  918. for i in line.split('\n'):
  919. parts = i.rsplit('|')
  920. head = ''.join(parts[:-1])
  921. tail = int(parts[-1] or 0)
  922. try:
  923. win.addstr(index, shift, head, tail | mod)
  924. except:
  925. win.addstr(min(curses.LINES - 1, index), min(curses.COLS - 1, shift), head, tail | mod)
  926. shift += len(head)
  927. def change_mode(self, mode):
  928. if not self.change_mode_allowed:
  929. return
  930. self.modes_data[self.active_mode][1] = self.focus_line_num
  931. self.modes_data[self.active_mode][2] = self.top_line_num
  932. self.old_mode = self.active_mode
  933. self.mode = self.mode.fromkeys(self.mode, 0)
  934. self.mode[mode] = 1
  935. self.focus_line_num = self.modes_data[mode][1]
  936. self.top_line_num = self.modes_data[mode][2]
  937. self.active_mode = mode
  938. self.get_data()
  939. def cycle_mode(self, direction=1):
  940. for mode, active in self.mode.items():
  941. if active == 1:
  942. self.change_mode((mode + direction) % 3)
  943. return
  944. def update_menu(self):
  945. if self.change_mode_allowed:
  946. self.menu = '{}|{}\n {}|{}\n {}|{}\n {:>{}}|{}'.format(
  947. self.menu_titles[0], curses.A_BOLD if self.mode[0] else curses.A_DIM,
  948. self.menu_titles[1], curses.A_BOLD if self.mode[1] else curses.A_DIM,
  949. self.menu_titles[2], curses.A_BOLD if self.mode[2] else curses.A_DIM,
  950. "? - help", self.cols - 30, curses.A_DIM)
  951. else:
  952. selected = 'output' if type(self.selected[0].pa) is PulseSinkInputInfo else 'input'
  953. self.menu = "Select new {} device:|{}".format(selected, curses.A_NORMAL)
  954. def update_info(self):
  955. focus, bottom = self.focus_line_num + self.top_line_num + 1, self.top_line_num + self.lines
  956. try:
  957. bar, side = self.data[focus - 1][0], self.data[focus - 1][1]
  958. except IndexError:
  959. self.focus_line_num, self.top_line_num = 0, 0
  960. for _ in range(len(self.data)): self.scroll(self.UP)
  961. return
  962. if side is Bar.NONE:
  963. self.info = str
  964. return
  965. side = 'All' if bar.locked else 'Mono' if bar.channels == 1 else self.SIDES[side]
  966. more = '↕' if bottom < self.n_lines and self.top_line_num > 0 else '↑' if self.top_line_num > 0 else '↓' if bottom < self.n_lines else ' '
  967. name = '{}: {}'.format(bar.name, side)
  968. if len(name) > self.cols - 8:
  969. name = '{}: {}'.format(bar.name[:self.cols - (10 + len(side))].strip(), side)
  970. locked = '{}|{}'.format(CFG.style.info_locked, self.red) if bar.locked else '{}|{}'.format(CFG.style.info_unlocked, curses.A_DIM)
  971. muted = '{}|{}'.format(CFG.style.info_muted, self.red) if bar.muted else '{}|{}'.format(CFG.style.info_unmuted, curses.A_DIM)
  972. self.info = '{}\n {}\n {}|{}\n{:>{}}|0'.format(locked, muted, name, curses.A_NORMAL, more, self.cols - len(name) - 5)
  973. def run_mouse(self):
  974. try:
  975. _, x, y, _, c = curses.getmouse()
  976. if c & curses.BUTTON1_CLICKED:
  977. if y > 0:
  978. top, bottom = self.top_line_num, len(self.data[self.top_line_num:self.top_line_num + self.lines]) - 1
  979. if y - 1 <= bottom:
  980. self.focus_line_num = max(top, min(bottom, y - 1))
  981. else:
  982. f1 = len(self.menu_titles[0]) + 1 # 1 is 'spacing' after the title
  983. f2 = f1 + len(self.menu_titles[1]) + 2
  984. f3 = f2 + len(self.menu_titles[2]) + 3
  985. if x in range(0, f1):
  986. self.change_mode(0)
  987. elif x in range(f1, f2):
  988. self.change_mode(1)
  989. elif x in range(f2, f3):
  990. self.change_mode(2)
  991. return c
  992. except curses.error:
  993. return None
  994. def resize(self):
  995. curses.COLS, curses.LINES = get_terminal_size()
  996. curses.resizeterm(curses.LINES, curses.COLS)
  997. self.submenu.resize(curses.LINES, self.submenu_width + 1)
  998. if self.submenu_show:
  999. self.submenu_show = False
  1000. self.focus_line_num = self.modes_data[5][1]
  1001. self.top_line_num = self.modes_data[5][2]
  1002. try:
  1003. self.helpwin.resize(14, 50)
  1004. self.helpwin.mvwin((curses.LINES // 2) - 7, (curses.COLS // 2) - 25)
  1005. except curses.error:
  1006. pass
  1007. self.helpwin_show = False
  1008. self.lines, self.cols = curses.LINES - 2, curses.COLS - 1
  1009. self.ev.set()
  1010. def terminate(self):
  1011. # if ^C pressed while sleeping in reconnect wrapper.restore won't be called
  1012. # so have to restore it manually here
  1013. self.screen.keypad(0)
  1014. curses.echo()
  1015. curses.nocbreak()
  1016. curses.endwin()
  1017. sys.exit()
  1018. def reconnect(self):
  1019. self.focus_line_num = 0
  1020. self.menu = self.info = str
  1021. self.data = [(Bar('PA - Connection refused.\nTrying to reconnect.'), Bar.NONE, 0)]
  1022. while not PULSE.connected:
  1023. self.display()
  1024. if self.screen.getch() in CFG.keys.quit: sys.exit()
  1025. PULSE.reconnect()
  1026. sleep(0.5)
  1027. PULSE.subscribe(self.wake_cb)
  1028. self.ev.set()
  1029. def run(self, _):
  1030. signal.signal(signal.SIGINT, lambda s, f: self.terminate())
  1031. signal.signal(signal.SIGTERM, lambda s, f: self.terminate())
  1032. signal.signal(signal.SIGWINCH, lambda s, f: self.resize())
  1033. threading.Thread(target=self.pregetcher, daemon=True).start()
  1034. PULSE.subscribe(self.wake_cb)
  1035. self.ev.set()
  1036. while True:
  1037. try:
  1038. if not self.submenu_show:
  1039. try:
  1040. self.get_data()
  1041. except RuntimeError:
  1042. self.reconnect()
  1043. except IndexError:
  1044. self.scroll(self.UP)
  1045. if self.helpwin_show:
  1046. self.display_helpwin()
  1047. self.run_helpwin()
  1048. continue
  1049. self.update_menu()
  1050. self.update_info()
  1051. self.display()
  1052. elif self.change_mode_allowed:
  1053. self.display_submenu()
  1054. self.run_submenu()
  1055. continue
  1056. except (curses.error, IndexError, ValueError) as e:
  1057. self.screen.erase()
  1058. self.screen.addstr("Terminal *might* be too small {}:{}\n".format(curses.LINES, curses.COLS))
  1059. self.screen.addstr("{}\n{}\n".format(str(self.mode), str(e)))
  1060. self.screen.addstr(str(traceback.extract_tb(e.__traceback__)))
  1061. c = self.getch()
  1062. if c == -1: continue
  1063. focus = self.top_line_num + self.focus_line_num
  1064. bar, side = self.data[focus][0], self.data[focus][1]
  1065. if c == self.KEY_MOUSE:
  1066. c = self.run_mouse() or c
  1067. if c in CFG.keys.mode1:
  1068. self.change_mode(0)
  1069. elif c in CFG.keys.mode2:
  1070. self.change_mode(1)
  1071. elif c in CFG.keys.mode3:
  1072. self.change_mode(2)
  1073. elif c == ord('?'):
  1074. self.helpwin_show = True
  1075. elif c == ord('\n'):
  1076. if not self.submenu_show and self.change_mode_allowed and side != Bar.NONE:
  1077. self.selected = self.data[focus]
  1078. if type(self.selected[0].pa) in (PulseSinkInfo, PulseSourceInfo):
  1079. self.submenu_data = ['Suspend', 'Resume', 'Set as default']
  1080. if self.selected[0].pa.n_ports:
  1081. self.submenu_data.append('Set port')
  1082. elif type(self.selected[0].pa) is PulseCard:
  1083. self.fill_submenu_pa(target='profile', off=0, hide=CFG.ui.hide_unavailable_profiles)
  1084. else:
  1085. self.submenu_data = ['Move', 'Kill']
  1086. self.submenu_show = True
  1087. self.modes_data[5][0] = 0
  1088. self.modes_data[5][1] = self.focus_line_num
  1089. self.modes_data[5][2] = self.top_line_num
  1090. self.focus_line_num = self.top_line_num = 0
  1091. self.n_lines = len(self.submenu_data)
  1092. self.resize_submenu()
  1093. elif not self.change_mode_allowed:
  1094. self.submenu_show = False
  1095. self.change_mode_allowed = True
  1096. if self.action == 'Move':
  1097. if type(self.selected[0].pa) is PulseSinkInputInfo:
  1098. PULSE.sink_input_move(self.selected[0].index, self.data[focus][0].pa.index)
  1099. elif type(self.selected[0].pa) is PulseSourceOutputInfo:
  1100. PULSE.source_output_move(self.selected[0].index, self.data[focus][0].pa.index)
  1101. self.change_mode(self.old_mode)
  1102. self.focus_line_num = self.modes_data[5][1]
  1103. self.top_line_num = self.modes_data[5][2]
  1104. else:
  1105. self.change_mode(self.old_mode)
  1106. elif c in CFG.keys.next_mode:
  1107. self.cycle_mode()
  1108. elif c in CFG.keys.prev_mode:
  1109. self.cycle_mode(direction=-1)
  1110. elif c in CFG.keys.quit:
  1111. if not self.change_mode_allowed:
  1112. self.submenu_show = False
  1113. self.change_mode_allowed = True
  1114. self.change_mode(self.old_mode)
  1115. self.focus_line_num = self.modes_data[5][1]
  1116. self.top_line_num = self.modes_data[5][2]
  1117. else:
  1118. sys.exit()
  1119. if side is Bar.NONE:
  1120. continue
  1121. if c in CFG.keys.up:
  1122. if bar.locked:
  1123. if self.data[focus][1] == 0:
  1124. n = 1
  1125. else:
  1126. n = self.data[focus][1] + 1
  1127. for _ in range(n): self.scroll(self.UP)
  1128. else:
  1129. self.scroll(self.UP)
  1130. if not self.data[self.top_line_num + self.focus_line_num][0]:
  1131. self.scroll(self.UP)
  1132. elif c in CFG.keys.down:
  1133. if bar.locked:
  1134. if self.data[focus][1] == self.data[focus][3] - 1:
  1135. n = 1
  1136. else:
  1137. n = ((self.data[focus][3] - 1) - self.data[focus][1]) + 1
  1138. for _ in range(n): self.scroll(self.DOWN)
  1139. else:
  1140. self.scroll(self.DOWN)
  1141. if not self.data[self.top_line_num + self.focus_line_num][0]:
  1142. self.scroll(self.DOWN)
  1143. elif c in CFG.keys.top:
  1144. self.scroll_first()
  1145. elif c in CFG.keys.bottom:
  1146. self.scroll_last()
  1147. elif c in CFG.keys.mute:
  1148. bar.mute_toggle()
  1149. elif c in CFG.keys.lock:
  1150. bar.lock_toggle()
  1151. elif c in CFG.keys.left or any([c & i for i in self.SCROLL_DOWN]):
  1152. bar.move(-CFG.general.step, side)
  1153. elif c in CFG.keys.right or any([c & i for i in self.SCROLL_UP]):
  1154. bar.move(CFG.general.step, side)
  1155. elif c in CFG.keys.left_big:
  1156. bar.move(-CFG.general.step_big, side)
  1157. elif c in CFG.keys.right_big:
  1158. bar.move(CFG.general.step_big, side)
  1159. elif c in self.DIGITS:
  1160. percent = int(chr(c)) * 10
  1161. bar.set(100 if percent == 0 else percent, side)
  1162. def fill_submenu_pa(self, target, off, hide):
  1163. self.submenu_data = []
  1164. active = getattr(self.selected[0].pa, "active_" + target).description.decode()
  1165. for i in getattr(self.selected[0].pa, target + "s"):
  1166. description = i.description.decode()
  1167. if active == description:
  1168. self.submenu_data.append(' {}|{}'.format(description, self.green))
  1169. else:
  1170. if hide and i.available == off: continue
  1171. self.submenu_data.append(' {}|{}'.format(description, curses.A_DIM if i.available == off else 0))
  1172. def build(self, target, devices, streams):
  1173. tmp = []
  1174. index = 0
  1175. for device in devices:
  1176. index += device.volume.channels
  1177. stream_index = device.volume.channels
  1178. tmp.append([device, device.volume.channels, index, stream_index])
  1179. device_index = len(tmp) - 1
  1180. for stream in streams:
  1181. if stream.owner == device.index:
  1182. index += stream.volume.channels
  1183. stream_index += stream.volume.channels
  1184. tmp.append([stream, -1, index, stream_index])
  1185. tmp[device_index][1] += stream.volume.channels
  1186. tmp[-1][1] = tmp[device_index][1]
  1187. for s in tmp:
  1188. found = False
  1189. for i, data in enumerate(target):
  1190. if s[0].index == data[2] and type(s[0]) == type(data[0].pa):
  1191. found = True
  1192. data[0].poll_data(s[0], s[1], s[3])
  1193. y = s[2] - (data[3] - data[1])
  1194. target[i], target[y] = target[y], target[i]
  1195. if not found:
  1196. bar = Bar(s[0])
  1197. bar.owned = s[1]
  1198. bar.stream_index = s[3]
  1199. for c in range(s[0].volume.channels):
  1200. target.append((bar, c, s[0].index, s[0].volume.channels))
  1201. for i in reversed(range(len(target))):
  1202. data = target[i]
  1203. for s in tmp:
  1204. if s[0].index == data[2] and type(s[0]) == type(data[0].pa):
  1205. y = s[2] - (data[3] - data[1])
  1206. target[i], target[y] = target[y], target[i]
  1207. break
  1208. else:
  1209. del target[i]
  1210. if self.focus_line_num + self.top_line_num >= i:
  1211. self.scroll(self.UP)
  1212. return target
  1213. def add_spacers(self, f):
  1214. tmp = []
  1215. l = len(f)
  1216. for i, s in enumerate(f):
  1217. tmp.append(s)
  1218. if s[0].stream_index == s[0].owned and s[1] == s[0].channels - 1 and i != l - 1:
  1219. tmp.append((None, -1, 0, 0))
  1220. return tmp
  1221. def get_data(self):
  1222. if self.mode[0]:
  1223. self.data = self.build(self.modes_data[0][0], PULSE.sink_list(), PULSE.sink_input_list())
  1224. self.data = self.add_spacers(self.data)
  1225. elif self.mode[1]:
  1226. ids = (b'org.PulseAudio.pavucontrol', b'org.gnome.VolumeControl', b'org.kde.kmixd', b'pulsemixer')
  1227. source_output_list = [s for s in PULSE.source_output_list() if s.application_id not in ids]
  1228. self.data = self.build(self.modes_data[1][0], PULSE.source_list(), source_output_list)
  1229. self.data = self.add_spacers(self.data)
  1230. elif self.mode[2]:
  1231. self.data = self.build(self.modes_data[2][0], PULSE.card_list(), [])
  1232. elif type(self.selected[0].pa) is PulseSinkInputInfo:
  1233. self.data = self.build(self.modes_data[3][0], PULSE.sink_list(), [])
  1234. elif type(self.selected[0].pa) is PulseSourceOutputInfo:
  1235. self.data = self.build(self.modes_data[4][0], PULSE.source_list(), [])
  1236. self.server_info = PULSE.get_server_info()
  1237. self.n_lines = len(self.data)
  1238. if not self.n_lines:
  1239. self.focus_line_num = 0
  1240. self.data.append((Bar('no data'), Bar.NONE, 0))
  1241. if not self.data[self.top_line_num + self.focus_line_num][0]:
  1242. self.scroll(self.UP)
  1243. def display(self):
  1244. self.screen.erase()
  1245. top = self.top_line_num
  1246. bottom = self.top_line_num + self.lines
  1247. self.display_line(0, self.menu)
  1248. for index, line in enumerate(self.data[top:bottom]):
  1249. bar, bartype = line[0], line[1]
  1250. if not bar:
  1251. self.screen.addstr(index + 1, 0, '', curses.A_DIM)
  1252. continue
  1253. elif bartype is Bar.NONE:
  1254. for i, name in enumerate(bar.name.split('\n')):
  1255. self.screen.addstr((self.lines // 2) + i, (self.cols // 2) - len(name) // 2, name, curses.A_DIM)
  1256. break
  1257. # hightlight lines from same bar
  1258. same = []
  1259. for i, v in enumerate(self.data[top:bottom]):
  1260. if v[0] is self.data[self.top_line_num + self.focus_line_num][0]:
  1261. same.append(v[0])
  1262. tree = ' '
  1263. if bar.owner == -1 and bar.owned > bar.channels:
  1264. tree = ' │'
  1265. if bar.owner != -1:
  1266. tree = ' │'
  1267. if bartype == Bar.LEFT:
  1268. if bar.owner == -1:
  1269. tree = ' '
  1270. if bar.owner != -1:
  1271. tree = ' ├─'
  1272. if bar.stream_index == bar.owned:
  1273. tree = ' └─'
  1274. if bar.channels != 1:
  1275. brackets = [CFG.style.bar_top_left, CFG.style.bar_top_right]
  1276. else:
  1277. brackets = [CFG.style.bar_left_mono, CFG.style.bar_right_mono]
  1278. elif bartype == bar.channels - 1:
  1279. if bar.stream_index == bar.owned:
  1280. tree = ' '
  1281. brackets = [CFG.style.bar_bottom_left, CFG.style.bar_bottom_right]
  1282. else:
  1283. if bar.stream_index == bar.owned:
  1284. tree = ' '
  1285. brackets = ['├', '┤']
  1286. # focus current lines
  1287. focus_hl, bracket_hl, arrow, gradient = 0, 0, CFG.style.arrow, self.gradient
  1288. if index == self.focus_line_num:
  1289. focus_hl = bracket_hl = curses.A_BOLD
  1290. arrow = CFG.style.arrow_focused
  1291. elif bar in same:
  1292. focus_hl = curses.A_BOLD
  1293. if bar.locked:
  1294. bracket_hl = curses.A_BOLD
  1295. arrow = CFG.style.arrow_locked
  1296. elif not bar.muted and self.color_mode != 2:
  1297. gradient = self.gray_gradient
  1298. # highlight chosen sink/source or muted
  1299. if not self.change_mode_allowed and self.selected[0].owner == self.data[index][0].index:
  1300. bracket_hl = self.green | bracket_hl
  1301. if bar.muted:
  1302. focus_hl = focus_hl | self.muted_color
  1303. elif bar.muted:
  1304. bracket_hl = bracket_hl | self.red
  1305. focus_hl = focus_hl | self.muted_color
  1306. off = 6 * (self.cols // (43 if self.cols <= 60 else 25)) - len(tree)
  1307. cols = self.cols - 31 - off - len(tree)
  1308. vol = list(CFG.style.bar_off * (cols - (cols % 3 != 0)))
  1309. n = int(len(vol) * bar.volume[bartype] / bar.maxsize)
  1310. if bar.muted:
  1311. vol[:n] = CFG.style.bar_on_muted * n
  1312. else:
  1313. vol[:n] = CFG.style.bar_on * n
  1314. vol = ''.join(vol)
  1315. if bartype is Bar.LEFT:
  1316. if bar.pa.name in (self.server_info.default_sink_name, self.server_info.default_source_name):
  1317. tree = CFG.style.default_stream
  1318. name = '{}{}'.format(bar.name, bar.media_name)
  1319. if bar.media_name_wide and len(bar.name) + sum(bar.media_name_widths) > 20 + off:
  1320. to_remove, widths = 0, [1] * len(bar.name) + bar.media_name_widths
  1321. while sum(widths) > 20 + off:
  1322. widths.pop()
  1323. to_remove += 1
  1324. name = name[:-to_remove].strip() + '~'
  1325. elif len(name) > 20 + off:
  1326. name = name[:20 + off].strip() + '~'
  1327. line = '{:<{}}|{}\n {:<3}|{}\n '.format(name, 22 + off, focus_hl,
  1328. '' if type(bar.pa) is PulseCard else bar.volume[0],
  1329. focus_hl)
  1330. elif bartype is Bar.RIGHT:
  1331. line = '{:>{}}|{}\n {}|{}\n {:<3}|{}\n '.format(
  1332. '', 21 + off, self.red if bar.locked else curses.A_DIM,
  1333. '', self.red if bar.muted else curses.A_DIM,
  1334. bar.volume[bartype], focus_hl)
  1335. else:
  1336. line = '{:>{}}{:<3}|{}\n '.format('', 23 + off, bar.volume[bartype], focus_hl)
  1337. if type(bar.pa) is PulseCard:
  1338. volbar = '\n{}|0'.format(bar.pa.active_profile.description.decode()[:len(vol)])
  1339. brackets = [' ', ' ']
  1340. else:
  1341. volbar = ''
  1342. for i, v in enumerate(re.findall('.{{{}}}'.format((len(vol) // 3)), vol)):
  1343. volbar += '\n{}|{}'.format(v, gradient[i] | focus_hl)
  1344. line += '{:>1}|{}\n{}|{}{}\n{}|{}\n{}|{}'.format(arrow, curses.A_BOLD,
  1345. brackets[0], bracket_hl,
  1346. volbar,
  1347. brackets[1], bracket_hl,
  1348. arrow, curses.A_BOLD)
  1349. self.display_line(index + 1, tree + "|0\n" + line)
  1350. self.display_line(self.lines + 1, self.info)
  1351. self.screen.refresh()
  1352. def get_mode_keys(self):
  1353. return [re.compile(r'[()]|KEY_').sub('', curses.keyname(k[0]).decode('utf-8')) for k in [
  1354. CFG.keys.mode1, CFG.keys.mode2, CFG.keys.mode3]]
  1355. def display_helpwin(self):
  1356. doc = (('j k ↑ ↓', 'Navigation'),
  1357. ('h l ← →', 'Change volume'),
  1358. ('H L Shift← Shift→', 'Change volume by 10'),
  1359. ('1 2 3 .. 8 9 0', 'Set volume to 10%-100%'),
  1360. ('m', 'Mute/Unmute'),
  1361. ('Space', 'Lock/Unlock channels'),
  1362. ('Enter', 'Context menu'),
  1363. ('{} {} {}'.format(*self.mode_keys), 'Change modes'),
  1364. ('Tab Shift Tab', 'Next/Previous mode'),
  1365. ('Mouse click', 'Select device or mode'),
  1366. ('Mouse wheel', 'Volume change'),
  1367. ('Esc q', 'Quit'))
  1368. win_width, desc_maxlen = self.helpwin.getmaxyx()[1] - 4, max(len(x[1]) for x in doc)
  1369. self.helpwin.erase()
  1370. for i, s in enumerate(doc):
  1371. self.helpwin.addstr(i + 1, 2, s[0] + ' ' * (win_width - desc_maxlen - len(s[0])) + s[1])
  1372. self.helpwin.border()
  1373. self.helpwin.refresh()
  1374. def run_helpwin(self):
  1375. if self.getch() in CFG.keys.quit:
  1376. self.helpwin_show = False
  1377. def resize_submenu(self):
  1378. key = lambda x: len(x.split('|')[0])
  1379. self.submenu_width = min(self.cols + 1, max(30, len(max(self.submenu_data, key=key).split('|')[0]) + 3))
  1380. self.submenu.resize(curses.LINES, self.submenu_width + 1)
  1381. def display_submenu(self):
  1382. top = self.top_line_num
  1383. bottom = self.top_line_num + self.lines + 2
  1384. self.submenu.erase()
  1385. self.submenu.vline(0, self.submenu_width, curses.ACS_VLINE, curses.LINES)
  1386. for index, line in enumerate(self.submenu_data[top:bottom]):
  1387. if index == self.focus_line_num:
  1388. focus_hl = curses.A_BOLD
  1389. arrow = CFG.style.arrow_focused
  1390. else:
  1391. focus_hl = curses.A_NORMAL
  1392. arrow = ' '
  1393. if '|' in line:
  1394. self.display_line(index, ' {}|0\n'.format(arrow) + line, focus_hl, win=self.submenu)
  1395. else:
  1396. self.submenu.addstr(index, 1, arrow + ' ' + line, focus_hl)
  1397. self.submenu.refresh()
  1398. def run_submenu(self):
  1399. c = self.getch()
  1400. if c in CFG.keys.quit:
  1401. self.submenu_show = False
  1402. self.focus_line_num = self.modes_data[5][1]
  1403. self.top_line_num = self.modes_data[5][2]
  1404. elif c in CFG.keys.up:
  1405. self.scroll(self.UP, cycle=True)
  1406. elif c in CFG.keys.down:
  1407. self.scroll(self.DOWN, cycle=True)
  1408. elif c in CFG.keys.top:
  1409. self.scroll_first()
  1410. elif c in CFG.keys.bottom:
  1411. self.scroll_last()
  1412. elif c == ord('\n'):
  1413. focus = self.focus_line_num + self.top_line_num
  1414. self.action = self.submenu_data[focus]
  1415. if self.action == 'Move':
  1416. if self.active_mode == 0:
  1417. self.change_mode(3)
  1418. elif self.active_mode == 1:
  1419. self.change_mode(4)
  1420. self.change_mode_allowed = self.submenu_show = False
  1421. return
  1422. elif self.action == 'Kill':
  1423. try:
  1424. PULSE.kill_client(self.selected[0].pa.client.index)
  1425. except:
  1426. if type(self.selected[0].pa) is PulseSinkInputInfo:
  1427. PULSE.kill_sink(self.selected[2])
  1428. else:
  1429. PULSE.kill_source(self.selected[2])
  1430. elif self.action == 'Suspend':
  1431. if type(self.selected[0].pa) is PulseSinkInfo:
  1432. PULSE.sink_suspend(self.selected[2], 1)
  1433. else:
  1434. PULSE.source_suspend(self.selected[2], 1)
  1435. elif self.action == 'Resume':
  1436. if type(self.selected[0].pa) is PulseSinkInfo:
  1437. PULSE.sink_suspend(self.selected[2], 0)
  1438. else:
  1439. PULSE.source_suspend(self.selected[2], 0)
  1440. elif self.action == 'Set as default':
  1441. if type(self.selected[0].pa) is PulseSinkInfo:
  1442. PULSE.set_default_sink(self.selected[0].pa.name)
  1443. else:
  1444. PULSE.set_default_source(self.selected[0].pa.name)
  1445. elif self.action == 'Set port':
  1446. self.fill_submenu_pa(target='port', off=1, hide=CFG.ui.hide_unavailable_ports)
  1447. self.focus_line_num = self.top_line_num = 0
  1448. self.n_lines = len(self.submenu_data)
  1449. return
  1450. else:
  1451. index = self.selected[0].pa.index
  1452. description = self.action.rsplit('|')[0].strip()
  1453. get_name = lambda desc, l: next(filter(lambda x: x.description.decode() == desc, l)).name
  1454. if type(self.selected[0].pa) is PulseSinkInfo:
  1455. PULSE.set_sink_port(index, get_name(description, self.selected[0].pa.ports))
  1456. elif type(self.selected[0].pa) is PulseSourceInfo:
  1457. PULSE.set_source_port(index, get_name(description, self.selected[0].pa.ports))
  1458. elif type(self.selected[0].pa) is PulseCard:
  1459. PULSE.set_card_profile(index, get_name(description, self.selected[0].pa.profiles))
  1460. self.change_mode_allowed = True
  1461. self.submenu_show = False
  1462. self.focus_line_num = self.modes_data[5][1]
  1463. self.top_line_num = self.modes_data[5][2]
  1464. def scroll(self, n, cycle=False):
  1465. next_line_num = self.focus_line_num + n
  1466. if n == self.UP and self.focus_line_num == 0 and self.top_line_num != 0:
  1467. self.top_line_num += self.UP
  1468. return
  1469. elif n == self.DOWN and next_line_num == self.lines and (self.top_line_num + self.lines) != self.n_lines:
  1470. self.top_line_num += self.DOWN
  1471. return
  1472. if n == self.UP:
  1473. if self.top_line_num != 0 or self.focus_line_num != 0:
  1474. self.focus_line_num = next_line_num
  1475. elif cycle:
  1476. self.scroll_last()
  1477. elif n == self.DOWN and self.focus_line_num != self.lines:
  1478. if self.top_line_num + self.focus_line_num + 1 != self.n_lines:
  1479. self.focus_line_num = next_line_num
  1480. elif cycle:
  1481. self.scroll_first()
  1482. def scroll_first(self):
  1483. for _ in range(self.n_lines): self.scroll(self.UP)
  1484. def scroll_last(self):
  1485. for _ in range(self.n_lines): self.scroll(self.DOWN)
  1486. class Config():
  1487. def __init__(self):
  1488. class General:
  1489. step = 1
  1490. step_big = 10
  1491. server = None
  1492. self._more_keys = {'KEY_ESC': 27, 'KEY_TAB': 9, 'C': -96, 'M': 128}
  1493. class Keys:
  1494. up = [ord('k'), curses.KEY_UP, curses.KEY_PPAGE]
  1495. down = [ord('j'), curses.KEY_DOWN, curses.KEY_NPAGE]
  1496. left = [ord('h'), curses.KEY_LEFT]
  1497. right = [ord('l'), curses.KEY_RIGHT]
  1498. left_big = [ord('H'), curses.KEY_SLEFT]
  1499. right_big = [ord('L'), curses.KEY_SRIGHT]
  1500. top = [ord('g'), curses.KEY_HOME]
  1501. bottom = [ord('G'), curses.KEY_END]
  1502. mode1 = [curses.KEY_F1]
  1503. mode2 = [curses.KEY_F2]
  1504. mode3 = [curses.KEY_F3]
  1505. next_mode = [self._more_keys['KEY_TAB']]
  1506. prev_mode = [curses.KEY_BTAB]
  1507. mute = [ord('m')]
  1508. lock = [ord(' ')]
  1509. quit = [ord('q'), self._more_keys['KEY_ESC']]
  1510. class UI:
  1511. hide_unavailable_profiles = False
  1512. hide_unavailable_ports = False
  1513. color = 2
  1514. mouse = True
  1515. class Style:
  1516. _bar_style = os.getenv('PULSEMIXER_BAR_STYLE', '┌╶┐╴└┘▮▯- ──').ljust(12, '?')
  1517. bar_top_left = _bar_style[0]
  1518. bar_left_mono = _bar_style[1]
  1519. bar_top_right = _bar_style[2]
  1520. bar_right_mono = _bar_style[3]
  1521. bar_bottom_left = _bar_style[4]
  1522. bar_bottom_right = _bar_style[5]
  1523. bar_on = _bar_style[6]
  1524. bar_on_muted = _bar_style[7]
  1525. bar_off = _bar_style[8]
  1526. arrow = _bar_style[9]
  1527. arrow_focused = _bar_style[10]
  1528. arrow_locked = _bar_style[11]
  1529. default_stream = '*'
  1530. info_locked = 'L'
  1531. info_unlocked = 'U'
  1532. info_muted = 'M'
  1533. info_unmuted = 'M'
  1534. self.general = General()
  1535. self.keys = Keys()
  1536. self.ui = UI()
  1537. self.style = Style()
  1538. self.renames = {}
  1539. self.path = os.getenv("XDG_CONFIG_HOME", os.path.join(os.path.expanduser("~"), ".config")) + '/pulsemixer.cfg'
  1540. def save(self):
  1541. default = '''
  1542. ;; Goes into ~/.config/pulsemixer.cfg, $XDG_CONFIG_HOME respected
  1543. ;; Everything that starts with "#" or ";" is a comment
  1544. ;; For the option to take effect simply uncomment it
  1545. [general]
  1546. step = 1
  1547. step-big = 10
  1548. ; server =
  1549. [keys]
  1550. ;; To bind "special keys" such as arrows see "Key constant" table in
  1551. ;; https://docs.python.org/3/library/curses.html#constants
  1552. ; up = k, KEY_UP, KEY_PPAGE
  1553. ; down = j, KEY_DOWN, KEY_NPAGE
  1554. ; left = h, KEY_LEFT
  1555. ; right = l, KEY_RIGHT
  1556. ; left-big = H, KEY_SLEFT
  1557. ; right-big = L, KEY_SRIGHT
  1558. ; top = g, KEY_HOME
  1559. ; bottom = G, KEY_END
  1560. ; mode1 = KEY_F1
  1561. ; mode2 = KEY_F2
  1562. ; mode3 = KEY_F3
  1563. ; next-mode = KEY_TAB
  1564. ; prev-mode = KEY_BTAB
  1565. ; mute = m
  1566. ; lock = ' ' ; 'space', quotes are stripped
  1567. ; quit = q, KEY_ESC
  1568. [ui]
  1569. ; hide-unavailable-profiles = no
  1570. ; hide-unavailable-ports = no
  1571. ; color = 2 ; same as --color, 0 no color, 1 color currently selected, 2 full-color
  1572. ; mouse = yes
  1573. [style]
  1574. ;; Pulsemixer will use these characters to draw interface
  1575. ;; Single characters only
  1576. ; bar-top-left = ┌
  1577. ; bar-left-mono = ╶
  1578. ; bar-top-right = ┐
  1579. ; bar-right-mono = ╴
  1580. ; bar-bottom-left = └
  1581. ; bar-bottom-right = ┘
  1582. ; bar-on = ▮
  1583. ; bar-on-muted = ▯
  1584. ; bar-off = -
  1585. ; arrow = ' '
  1586. ; arrow-focused = ─
  1587. ; arrow-locked = ─
  1588. ; default-stream = *
  1589. ; info-locked = L
  1590. ; info-unlocked = U
  1591. ; info-muted = M ; 🔇
  1592. ; info-unmuted = M ; 🔉
  1593. [renames]
  1594. ;; Changes stream names in interactive mode, regular expression are supported
  1595. ;; https://docs.python.org/3/library/re.html#regular-expression-syntax
  1596. ; 'default name example' = 'new name'
  1597. ; '(?i)built-in .* audio' = 'Audio Controller'
  1598. ; 'AudioIPC Server' = 'Firefox'
  1599. '''
  1600. directory = self.path.rsplit('/', 1)[0]
  1601. if not os.path.exists(directory):
  1602. os.makedirs(directory)
  1603. with open(self.path, 'w') as configfile:
  1604. configfile.write(dedent(default).strip())
  1605. return self.path
  1606. def load(self):
  1607. parser = ConfigParser(inline_comment_prefixes=('#', ';'), empty_lines_in_values=False)
  1608. parser.optionxform = str # keep case of keys, lowered() later
  1609. parser.NONSPACECRE = re.compile(r"") # ignore leading whitespace
  1610. if not parser.read(self.path): return self
  1611. if parser.has_section('renames'):
  1612. self.renames = {re.compile(k.strip('"\'') + r'\Z'):v.strip('"\'') for k, v in parser.items('renames')}
  1613. parser.remove_section('renames')
  1614. def getkeys(s, k):
  1615. keys = []
  1616. for i in parser.get(s, k).strip(',').split(','):
  1617. i = i.strip().strip('"\'') # in case 'key' is encountered
  1618. if len(i) > 1:
  1619. if i.startswith(('C-', 'M-')):
  1620. mod, key = i.split('-')
  1621. key = self._more_keys[mod] + ord(key.lower())
  1622. else:
  1623. key = getattr(curses, i, self._more_keys.get(i))
  1624. else:
  1625. key = ord(i)
  1626. if key is None: raise Exception("module 'curses' has no attribute {}".format(i))
  1627. keys.append(key)
  1628. return keys
  1629. get = {str: lambda s, k: parser.get(s, k).strip('"\''),
  1630. None.__class__: lambda s, k: parser.get(s, k).encode(), # server
  1631. list: getkeys, bool: parser.getboolean,
  1632. int: parser.getint, float: parser.getfloat}
  1633. for section in parser.sections():
  1634. for key in parser[section]:
  1635. pykey = key.lower().replace('-', '_')
  1636. pyval = getattr(getattr(self, section.lower()), pykey)
  1637. val = get[type(pyval)](section, key)
  1638. setattr(getattr(self, section.lower()), pykey, val)
  1639. return self
  1640. PULSE = CFG = None
  1641. def main():
  1642. try:
  1643. opts, args = getopt.getopt(
  1644. sys.argv[1:], "hvl",
  1645. ["help", "version", "list", "list-sinks", "list-sources", "id=",
  1646. "set-volume=", "set-volume-all=", "change-volume=", "max-volume=",
  1647. "get-mute", "toggle-mute", "mute", "unmute", "get-volume",
  1648. "color=", "server=", "no-mouse", "create-config"])
  1649. except getopt.GetoptError as e:
  1650. sys.exit("ERR: {}".format(e))
  1651. assert args == [], sys.exit('ERR: {} not not recognized'.format(' '.join(args).strip()))
  1652. dopts = dict(opts)
  1653. if '-h' in dopts or '--help' in dopts:
  1654. sys.exit(print(__doc__))
  1655. if '-v' in dopts or '--version' in dopts:
  1656. sys.exit(print(VERSION))
  1657. if '--create-config' in dopts:
  1658. try:
  1659. sys.exit(print(Config().save()))
  1660. except Exception as e: # permission denied and such
  1661. sys.exit('ERR: {}'.format(e))
  1662. global PULSE, CFG
  1663. try:
  1664. CFG = Config().load()
  1665. except Exception as e:
  1666. sys.exit('ERR: {}'.format(e))
  1667. CFG.general.server = dopts.get('--server', '').encode() or CFG.general.server
  1668. CFG.ui.mouse = False if '--no-mouse' in dopts else CFG.ui.mouse
  1669. try:
  1670. CFG.ui.color = min(2, max(0, int(dopts.get('--color', '') or CFG.ui.color)))
  1671. except:
  1672. sys.exit('ERR: color must be a number')
  1673. signal.signal(signal.SIGINT, lambda s, f: sys.exit(1))
  1674. PULSE = Pulse('pulsemixer', CFG.general.server)
  1675. noninteractive_opts = dict(dopts)
  1676. noninteractive_opts.pop('--server', None)
  1677. noninteractive_opts.pop('--color', None)
  1678. noninteractive_opts.pop('--no-mouse', None)
  1679. if not noninteractive_opts:
  1680. if not sys.stdout.isatty(): sys.exit('ERR: output is not a tty-like device')
  1681. title = 'pulsemixer {}'.format(CFG.general.server.decode() if CFG.general.server else '')
  1682. print('\033]2;{}\007'.format(title.strip()), end='', flush=True)
  1683. curses.wrapper(Screen(CFG.ui.color, CFG.ui.mouse).run)
  1684. sinks = PULSE.sink_list()
  1685. sink_inputs = PULSE.sink_input_list()
  1686. sources = PULSE.source_list()
  1687. source_outputs = PULSE.source_output_list()
  1688. server_info = PULSE.get_server_info()
  1689. streams = OrderedDict()
  1690. for k, v in (('sink-', sinks), ('sink-input-', sink_inputs), ('source-', sources), ('source-output-', source_outputs)):
  1691. for stream in v: streams[k + str(stream.index)] = stream
  1692. check_n = lambda x, err: x.strip('+-').isdigit() or sys.exit('ERR: {} must be a number'.format(err))
  1693. check_id = lambda x: x in streams or sys.exit('ERR: No such ID: ' + str(x))
  1694. from_old_id = lambda index: next((k for k in streams if k.rsplit('-', 1)[-1] == index), index)
  1695. print_default = lambda x, y: print(x == y and ', Default' or '')
  1696. if '--id' in dopts:
  1697. index = [i for i in opts if '--id' in i][0][1]
  1698. if index.isdigit(): index = from_old_id(index)
  1699. else:
  1700. index = 'sink-{}'.format([s.index for s in sinks if s.name == server_info.default_sink_name][0])
  1701. check_id(index)
  1702. max_volume = 150
  1703. for opt, arg in opts:
  1704. if opt == '--id':
  1705. index = arg
  1706. if index.isdigit(): index = from_old_id(index)
  1707. check_id(index)
  1708. max_volume = 150 # reset for each new id
  1709. elif opt in ('-l', '--list'):
  1710. for sink in sinks:
  1711. print("Sink:\t\t", sink, end='')
  1712. print_default(sink.name, server_info.default_sink_name)
  1713. for sink in sink_inputs:
  1714. print("Sink input:\t", sink)
  1715. for source in sources:
  1716. print("Source:\t\t", source, end='')
  1717. print_default(source.name, server_info.default_source_name)
  1718. for source in source_outputs:
  1719. print("Source output:\t", source)
  1720. elif opt == '--list-sinks':
  1721. for sink in sinks:
  1722. print("Sink:\t\t", sink, end='')
  1723. print_default(sink.name, server_info.default_sink_name)
  1724. for sink in sink_inputs:
  1725. print("Sink input:\t", sink)
  1726. elif opt == '--list-sources':
  1727. for source in sources:
  1728. print("Source:\t\t", source, end='')
  1729. print_default(source.name, server_info.default_source_name)
  1730. for source in source_outputs:
  1731. print("Source output:\t", source)
  1732. elif opt == '--get-mute':
  1733. print(streams[index].mute)
  1734. elif opt == '--mute':
  1735. PULSE.mute_stream(streams[index])
  1736. elif opt == '--unmute':
  1737. PULSE.unmute_stream(streams[index])
  1738. elif opt == '--toggle-mute':
  1739. PULSE.unmute_stream(streams[index]) if streams[index].mute else PULSE.mute_stream(streams[index])
  1740. elif opt == '--get-volume':
  1741. print(*streams[index].volume.values)
  1742. elif opt == '--set-volume':
  1743. check_n(arg, err='volume')
  1744. vol = streams[index].volume
  1745. for i, _ in enumerate(vol.values):
  1746. vol.values[i] = int(arg)
  1747. PULSE.set_volume(streams[index], vol)
  1748. elif opt == '--set-volume-all':
  1749. vol = streams[index].volume
  1750. arg = arg.strip(':').split(':')
  1751. if len(arg) != len(vol.values):
  1752. sys.exit("ERR: Specified volumes not equal to the number of channels in the stream")
  1753. for i, _ in enumerate(vol.values):
  1754. check_n(arg[i], err='volume')
  1755. vol.values[i] = int(arg[i])
  1756. PULSE.set_volume(streams[index], vol)
  1757. elif opt == '--change-volume':
  1758. check_n(arg, err='volume')
  1759. vol = streams[index].volume
  1760. for i, _ in enumerate(vol.values):
  1761. vol.values[i] = min(vol.values[i] + int(arg), max_volume)
  1762. PULSE.set_volume(streams[index], vol)
  1763. elif opt == '--max-volume':
  1764. check_n(arg, err='max volume')
  1765. max_volume = int(arg)
  1766. vol = streams[index].volume
  1767. for i, _ in enumerate(vol.values):
  1768. vol.values[i] = min(vol.values[i], max_volume)
  1769. PULSE.set_volume(streams[index], vol)
  1770. if __name__ == '__main__':
  1771. main()