jack-plumber 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #!/usr/bin/env python
  2. # PYTHON_ARGCOMPLETE_OK
  3. import re
  4. import jack
  5. import datetime
  6. import subprocess
  7. import ioex.shell
  8. from gi.repository import GLib
  9. def log(message, color = ioex.shell.TextColor.default):
  10. print(''.join([
  11. color,
  12. ioex.shell.Formatting.dim,
  13. str(datetime.datetime.now()),
  14. ': ',
  15. ioex.shell.Formatting.reset_dim,
  16. message,
  17. ioex.shell.TextColor.default,
  18. ]))
  19. class PortEventType:
  20. preexisting = 'preexisting'
  21. registered = 'registered'
  22. renamed = 'renamed'
  23. unregistered = 'unregistered'
  24. class Instruction(object):
  25. def execute(self, client, port, event, date):
  26. pass
  27. def __repr__(self):
  28. return type(self).__name__ + '(' + ', '.join([a + ': ' + repr(getattr(self, a)) for a in vars(self)]) + ')'
  29. class PortRenameInstruction(Instruction):
  30. def __init__(self, new_port_name):
  31. self.new_port_name = new_port_name
  32. def execute(self, client, port, event, date):
  33. if event in [PortEventType.registered, PortEventType.preexisting]:
  34. port.set_short_name(self.new_port_name)
  35. class PortConnectInstruction(Instruction):
  36. def __init__(self, other_client_pattern, other_port_pattern):
  37. self.other_client_pattern = other_client_pattern
  38. self.other_port_pattern = other_port_pattern
  39. def execute(self, client, port, event, date):
  40. if event in [PortEventType.registered, PortEventType.renamed, PortEventType.preexisting]:
  41. for other_port in [
  42. p for p in client.get_ports()
  43. if re.match(self.other_client_pattern, p.get_client_name())
  44. and re.match(self.other_port_pattern, p.get_short_name())
  45. ]:
  46. try:
  47. if port.is_output():
  48. client.connect(port, other_port)
  49. else:
  50. client.connect(other_port, port)
  51. except jack.ConnectionExists:
  52. pass
  53. class ExecuteCommandInstruction(Instruction):
  54. def __init__(self, command, events):
  55. self.command = command
  56. self.events = events
  57. def execute(self, client, port, event, date):
  58. if event in self.events:
  59. log("port '%s' %s: execute command '%s'" % (port.get_name(), event, self.command), ioex.shell.TextColor.yellow)
  60. subprocess.call(self.command, shell = True)
  61. def check_port(client, port, event, instructions):
  62. date = datetime.datetime.now()
  63. log("port '%s' %s" % (port.get_name(), event))
  64. for client_pattern in instructions:
  65. if re.match(client_pattern, port.get_client_name()):
  66. for port_pattern in instructions[client_pattern]:
  67. if re.match(port_pattern, port.get_short_name()):
  68. for instruction in instructions[client_pattern][port_pattern]:
  69. GLib.idle_add(instruction.execute, client, port, event, date)
  70. def port_registered(client, port, instructions):
  71. check_port(client, port, PortEventType.registered, instructions)
  72. def port_unregistered(client, port, instructions):
  73. check_port(client, port, PortEventType.unregistered, instructions)
  74. def port_renamed(client, port, old_name, new_name, instructions):
  75. """ Avoid recursion by skipping rename instructions. """
  76. check_port(client, port, PortEventType.renamed, instructions)
  77. def server_shutdown(client, reason, callback):
  78. print(reason)
  79. log("jack client shutdown due to '%s'" % (reason), ioex.shell.TextColor.red)
  80. if callback:
  81. GLib.idle_add(callback)
  82. def create_client(instructions, server_shutdown_callback = None):
  83. jack_client = jack.Client('plumber')
  84. jack_client.set_port_registered_callback(port_registered, instructions)
  85. jack_client.set_port_unregistered_callback(port_unregistered, instructions)
  86. jack_client.set_port_renamed_callback(port_renamed, instructions)
  87. if server_shutdown_callback:
  88. jack_client.set_shutdown_callback(server_shutdown, server_shutdown_callback)
  89. jack_client.activate()
  90. for port in jack_client.get_ports():
  91. check_port(jack_client, port, PortEventType.preexisting, instructions)
  92. return jack_client
  93. def _init_argparser():
  94. import argparse
  95. argparser = argparse.ArgumentParser(description = None)
  96. argparser.add_argument(
  97. '-c', '--config',
  98. metavar = 'path',
  99. dest = 'config_path',
  100. help = 'path to config file',
  101. )
  102. argparser.add_argument(
  103. '--dbus',
  104. action='store_true',
  105. help = 'wait for jack server to start / restart via jackdbus',
  106. )
  107. argparser.add_argument(
  108. '--rename-port',
  109. dest = 'port_renaming_instructions',
  110. action = 'append',
  111. nargs = 3,
  112. metavar = ('client_name_pattern', 'port_name_pattern', 'new_port_name'),
  113. )
  114. argparser.add_argument(
  115. '--connect-ports',
  116. dest = 'port_connecting_instructions',
  117. action = 'append',
  118. nargs = 4,
  119. metavar = (
  120. 'client_name_pattern_1',
  121. 'port_name_pattern_1',
  122. 'client_name_pattern_2',
  123. 'port_name_pattern_2',
  124. ),
  125. )
  126. argparser.add_argument(
  127. '--execute-command',
  128. dest = 'command_execution_instructions',
  129. action = 'append',
  130. nargs = 4,
  131. metavar = ('client_name_pattern', 'port_name_pattern', 'event', 'command'),
  132. help = 'possible events: ' + ', '.join([
  133. PortEventType.preexisting,
  134. PortEventType.registered,
  135. PortEventType.renamed,
  136. PortEventType.unregistered,
  137. ]),
  138. )
  139. return argparser
  140. def register_instruction(client_pattern, port_pattern, instruction, instructions):
  141. if not client_pattern in instructions:
  142. instructions[client_pattern] = {}
  143. if not port_pattern in instructions[client_pattern]:
  144. instructions[client_pattern][port_pattern] = []
  145. instructions[client_pattern][port_pattern].append(instruction)
  146. def main(argv):
  147. argparser = _init_argparser()
  148. try:
  149. import argcomplete
  150. argcomplete.autocomplete(argparser)
  151. except ImportError:
  152. pass
  153. args = argparser.parse_args(argv)
  154. instructions = {}
  155. if args.config_path:
  156. import yaml
  157. with open(args.config_path) as config_file:
  158. config = yaml.load(config_file.read())
  159. for instruction in config['instructions']:
  160. if instruction['type'] == 'rename port':
  161. register_instruction(
  162. instruction['client_pattern'],
  163. instruction['port_pattern'],
  164. PortRenameInstruction(instruction['new_port_name']),
  165. instructions,
  166. )
  167. elif instruction['type'] == 'connect ports':
  168. register_instruction(
  169. instruction['client_pattern_1'],
  170. instruction['port_pattern_1'],
  171. PortConnectInstruction(instruction['client_pattern_2'], instruction['port_pattern_2']),
  172. instructions,
  173. )
  174. register_instruction(
  175. instruction['client_pattern_2'],
  176. instruction['port_pattern_2'],
  177. PortConnectInstruction(instruction['client_pattern_1'], instruction['port_pattern_1']),
  178. instructions,
  179. )
  180. elif instruction['type'] == 'execute command':
  181. if not 'events' in instruction:
  182. instruction['events'] = [instruction['event']]
  183. register_instruction(
  184. instruction['client_pattern'],
  185. instruction['port_pattern'],
  186. ExecuteCommandInstruction(instruction['command'], instruction['events']),
  187. instructions,
  188. )
  189. if args.port_renaming_instructions:
  190. for renaming_instruction in args.port_renaming_instructions:
  191. (client_pattern, port_pattern, new_port_name) = renaming_instruction
  192. register_instruction(
  193. client_pattern,
  194. port_pattern,
  195. PortRenameInstruction(new_port_name),
  196. instructions,
  197. )
  198. if args.port_connecting_instructions:
  199. for connecting_instruction in args.port_connecting_instructions:
  200. (client_pattern_1, port_pattern_1, client_pattern_2, port_pattern_2) = connecting_instruction
  201. register_instruction(
  202. client_pattern_1,
  203. port_pattern_1,
  204. PortConnectInstruction(client_pattern_2, port_pattern_2),
  205. instructions,
  206. )
  207. register_instruction(
  208. client_pattern_2,
  209. port_pattern_2,
  210. PortConnectInstruction(client_pattern_1, port_pattern_1),
  211. instructions,
  212. )
  213. if args.command_execution_instructions:
  214. for execution_instruction in args.command_execution_instructions:
  215. (client_pattern, port_pattern, event, command) = execution_instruction
  216. register_instruction(
  217. client_pattern,
  218. port_pattern,
  219. ExecuteCommandInstruction(command, [event]),
  220. instructions,
  221. )
  222. loop = GLib.MainLoop()
  223. loop_attr = {'client': None}
  224. if args.dbus:
  225. import dbus
  226. from dbus.mainloop.glib import DBusGMainLoop
  227. session_bus = dbus.SessionBus(mainloop = DBusGMainLoop())
  228. jack_dbus_interface = dbus.Interface(
  229. session_bus.get_object(
  230. 'org.jackaudio.service',
  231. '/org/jackaudio/Controller',
  232. ),
  233. dbus_interface = 'org.jackaudio.JackControl',
  234. )
  235. def shutdown_callback():
  236. loop_attr['client'] = None
  237. def jack_started():
  238. log("detected start of jack server", ioex.shell.TextColor.green)
  239. loop_attr['client'] = create_client(instructions, shutdown_callback)
  240. jack_dbus_interface.connect_to_signal('ServerStarted', jack_started)
  241. if jack_dbus_interface.IsStarted():
  242. loop_attr['client'] = create_client(instructions, shutdown_callback)
  243. else:
  244. loop_attr['client'] = create_client(instructions, lambda: loop.quit())
  245. try:
  246. loop.run()
  247. except KeyboardInterrupt:
  248. log('keyboard interrupt', ioex.shell.TextColor.red)
  249. pass
  250. return 0
  251. if __name__ == "__main__":
  252. import sys
  253. sys.exit(main(sys.argv[1:]))