Audio plugin host https://kx.studio/carla
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

carla_control.py 17KB

11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. # Carla Backend code (OSC stuff)
  4. # Copyright (C) 2011-2015 Filipe Coelho <falktx@falktx.com>
  5. #
  6. # This program is free software; you can redistribute it and/or
  7. # modify it under the terms of the GNU General Public License as
  8. # published by the Free Software Foundation; either version 2 of
  9. # the License, or any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # For a full copy of the GNU General Public License see the doc/GPL.txt file.
  17. # ------------------------------------------------------------------------------------------------------------
  18. # Imports (Custom)
  19. from carla_host import *
  20. # ------------------------------------------------------------------------------------------------------------
  21. # Imports (liblo)
  22. from liblo import make_method, Address, ServerError, ServerThread
  23. from liblo import send as lo_send
  24. from liblo import TCP as LO_TCP
  25. from liblo import UDP as LO_UDP
  26. from random import random
  27. # ------------------------------------------------------------------------------------------------------------
  28. # Global liblo objects
  29. global lo_target, lo_target_name
  30. lo_target = None
  31. lo_target_name = ""
  32. # ------------------------------------------------------------------------------------------------------------
  33. # Host OSC object
  34. class CarlaHostOSC(CarlaHostQtPlugin):
  35. def __init__(self):
  36. CarlaHostQtPlugin.__init__(self)
  37. # -------------------------------------------------------------------
  38. def printAndReturnError(self, error):
  39. print(error)
  40. self.fLastError = error
  41. return False
  42. def sendMsg(self, lines):
  43. global lo_target, lo_target_name
  44. if lo_target is None:
  45. return self.printAndReturnError("lo_target is None")
  46. if lo_target_name is None:
  47. return self.printAndReturnError("lo_target_name is None")
  48. if len(lines) < 2:
  49. return self.printAndReturnError("not enough arguments")
  50. method = lines.pop(0)
  51. if method not in (
  52. #"set_option",
  53. "set_active",
  54. "set_drywet",
  55. "set_volume",
  56. "set_balance_left",
  57. "set_balance_right",
  58. "set_panning",
  59. #"set_ctrl_channel",
  60. "set_parameter_value",
  61. "set_parameter_midi_channel",
  62. "set_parameter_midi_cc",
  63. "set_program",
  64. "set_midi_program",
  65. #"set_custom_data",
  66. #"set_chunk_data",
  67. #"prepare_for_save",
  68. #"reset_parameters",
  69. #"randomize_parameters",
  70. "send_midi_note"
  71. ):
  72. return self.printAndReturnError("invalid method '%s'" % method)
  73. pluginId = lines.pop(0)
  74. args = []
  75. if method == "send_midi_note":
  76. channel, note, velocity = lines
  77. if velocity:
  78. method = "note_on"
  79. args = [channel, note, velocity]
  80. else:
  81. method = "note_off"
  82. args = [channel, note]
  83. else:
  84. for line in lines:
  85. if isinstance(line, bool):
  86. args.append(int(line))
  87. else:
  88. args.append(line)
  89. path = "/%s/%i/%s" % (lo_target_name, pluginId, method)
  90. print(path, args)
  91. lo_send(lo_target, path, *args)
  92. return True
  93. # -------------------------------------------------------------------
  94. def engine_init(self, driverName, clientName):
  95. global lo_target
  96. return lo_target is not None
  97. def engine_close(self):
  98. return True
  99. def engine_idle(self):
  100. return
  101. def is_engine_running(self):
  102. global lo_target
  103. return lo_target is not None
  104. def set_engine_about_to_close(self):
  105. return
  106. # ------------------------------------------------------------------------------------------------------------
  107. # OSC Control server
  108. class CarlaControlServerThread(ServerThread):
  109. def __init__(self, host, mode):
  110. ServerThread.__init__(self, 8998 + int(random()*9000), mode)
  111. self.host = host
  112. def getFullURL(self):
  113. return "%scarla-control" % self.get_url()
  114. @make_method('/carla-control/add_plugin_start', 'is') # FIXME skip name
  115. def add_plugin_start_callback(self, path, args):
  116. print(path, args)
  117. pluginId, pluginName = args
  118. self.host._add(pluginId)
  119. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  120. @make_method('/carla-control/add_plugin_end', 'i') # FIXME skip name
  121. def add_plugin_end_callback(self, path, args):
  122. print(path, args)
  123. pluginId, = args
  124. self.host.PluginAddedCallback.emit(pluginId, "") #self.fPluginsInfo[pluginId].pluginInfo['name'])
  125. @make_method('/carla-control/remove_plugin', 'i')
  126. def remove_plugin_callback(self, path, args):
  127. print(path, args)
  128. pluginId, = args
  129. self.host.PluginRemovedCallback.emit(pluginId)
  130. @make_method('/carla-control/set_plugin_info1', 'iiiih')
  131. def set_plugin_info1_callback(self, path, args):
  132. print(path, args)
  133. pluginId, type_, category, hints, uniqueId = args # , optsAvail, optsEnabled
  134. optsAvail = optsEnabled = 0x0 # FIXME
  135. hints &= ~PLUGIN_HAS_CUSTOM_UI
  136. pinfo = {
  137. 'type': type_,
  138. 'category': category,
  139. 'hints': hints,
  140. 'optionsAvailable': optsAvail,
  141. 'optionsEnabled': optsEnabled,
  142. 'uniqueId': uniqueId
  143. }
  144. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  145. @make_method('/carla-control/set_plugin_info2', 'issss')
  146. def set_plugin_info2_callback(self, path, args):
  147. print(path, args)
  148. pluginId, realName, label, maker, copyright = args # , filename, name, iconName
  149. filename = name = iconName = "" # FIXME
  150. pinfo = {
  151. 'filename': filename,
  152. #'name': name, # FIXME
  153. 'label': label,
  154. 'maker': maker,
  155. 'copyright': copyright,
  156. 'iconName': iconName
  157. }
  158. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  159. self.host._set_pluginRealName(pluginId, realName)
  160. @make_method('/carla-control/set_audio_count', 'iii')
  161. def set_audio_count_callback(self, path, args):
  162. print(path, args)
  163. pluginId, ins, outs = args
  164. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  165. @make_method('/carla-control/set_midi_count', 'iii')
  166. def set_midi_count_callback(self, path, args):
  167. print(path, args)
  168. pluginId, ins, outs = args
  169. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  170. @make_method('/carla-control/set_parameter_count', 'iii') # FIXME
  171. def set_parameter_count_callback(self, path, args):
  172. print(path, args)
  173. pluginId, ins, outs = args # , count
  174. count = ins + outs
  175. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  176. @make_method('/carla-control/set_program_count', 'ii')
  177. def set_program_count_callback(self, path, args):
  178. print(path, args)
  179. pluginId, count = args
  180. self.host._set_programCount(pluginId, count)
  181. @make_method('/carla-control/set_midi_program_count', 'ii')
  182. def set_midi_program_count_callback(self, path, args):
  183. print(path, args)
  184. pluginId, count = args
  185. self.host._set_midiProgramCount(pluginId, count)
  186. @make_method('/carla-control/set_parameter_data', 'iiiiss')
  187. def set_parameter_data_callback(self, path, args):
  188. print(path, args)
  189. pluginId, paramId, type_, hints, name, unit = args
  190. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  191. paramInfo = {
  192. 'name': name,
  193. 'symbol': "",
  194. 'unit': unit,
  195. 'scalePointCount': 0,
  196. }
  197. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  198. paramData = {
  199. 'type': type_,
  200. 'hints': hints,
  201. 'index': paramId,
  202. 'rindex': -1,
  203. 'midiCC': -1,
  204. 'midiChannel': 0
  205. }
  206. self.host._set_parameterData(pluginId, paramId, paramData)
  207. @make_method('/carla-control/set_parameter_ranges1', 'iifff')
  208. def set_parameter_ranges1_callback(self, path, args):
  209. print(path, args)
  210. pluginId, paramId, def_, min_, max_ = args
  211. paramRanges = {
  212. 'def': def_,
  213. 'min': min_,
  214. 'max': max_
  215. }
  216. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  217. @make_method('/carla-control/set_parameter_ranges2', 'iifff')
  218. def set_parameter_ranges2_callback(self, path, args):
  219. print(path, args)
  220. pluginId, paramId, step, stepSmall, stepLarge = args
  221. paramRanges = {
  222. 'step': step,
  223. 'stepSmall': stepSmall,
  224. 'stepLarge': stepLarge
  225. }
  226. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  227. @make_method('/carla-control/set_parameter_midi_cc', 'iii')
  228. def set_parameter_midi_cc_callback(self, path, args):
  229. print(path, args)
  230. pluginId, paramId, cc = args
  231. self.host._set_parameterMidiCC(pluginId, paramId, cc)
  232. self.host.ParameterMidiCcChangedCallback.emit(pluginId, paramId, cc)
  233. @make_method('/carla-control/set_parameter_midi_channel', 'iii')
  234. def set_parameter_midi_channel_callback(self, path, args):
  235. print(path, args)
  236. pluginId, paramId, channel = args
  237. self.host._set_parameterMidiChannel(pluginId, paramId, channel)
  238. self.host.ParameterMidiChannelChangedCallback.emit(pluginId, paramId, channel)
  239. @make_method('/carla-control/set_parameter_value', 'iif')
  240. def set_parameter_value_callback(self, path, args):
  241. pluginId, paramId, paramValue = args
  242. if paramId < 0:
  243. self.host._set_internalValue(pluginId, paramId, paramValue)
  244. else:
  245. self.host._set_parameterValue(pluginId, paramId, paramValue)
  246. self.host.ParameterValueChangedCallback.emit(pluginId, paramId, paramValue)
  247. @make_method('/carla-control/set_default_value', 'iif')
  248. def set_default_value_callback(self, path, args):
  249. print(path, args)
  250. pluginId, paramId, paramValue = args
  251. self.host._set_parameterDefault(pluginId, paramId, paramValue)
  252. self.host.ParameterDefaultChangedCallback.emit(pluginId, paramId, paramValue)
  253. @make_method('/carla-control/set_current_program', 'ii')
  254. def set_current_program_callback(self, path, args):
  255. print(path, args)
  256. pluginId, current = args
  257. self.host._set_currentProgram(pluginId, current)
  258. self.host.ProgramChangedCallback.emit(current)
  259. @make_method('/carla-control/set_current_midi_program', 'ii')
  260. def set_current_midi_program_callback(self, path, args):
  261. print(path, args)
  262. pluginId, current = args
  263. self.host._set_currentMidiProgram(pluginId, current)
  264. #self.host.MidiProgramChangedCallback.emit() # FIXME
  265. @make_method('/carla-control/set_program_name', 'iis')
  266. def set_program_name_callback(self, path, args):
  267. print(path, args)
  268. pluginId, progId, progName = args
  269. self.host._set_programName(pluginId, progId, progName)
  270. @make_method('/carla-control/set_midi_program_data', 'iiiis')
  271. def set_midi_program_data_callback(self, path, args):
  272. print(path, args)
  273. pluginId, midiProgId, bank, program, name = args
  274. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  275. @make_method('/carla-control/note_on', 'iiii')
  276. def set_note_on_callback(self, path, args):
  277. print(path, args)
  278. pluginId, channel, note, velocity = args
  279. self.host.NoteOnCallback.emit(pluginId, channel, note, velocity)
  280. @make_method('/carla-control/note_off', 'iii')
  281. def set_note_off_callback(self, path, args):
  282. print(path, args)
  283. pluginId, channel, note = args
  284. self.host.NoteOffCallback.emit(pluginId, channel, note)
  285. @make_method('/carla-control/set_peaks', 'iffff')
  286. def set_peaks_callback(self, path, args):
  287. pluginId, in1, in2, out1, out2 = args
  288. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  289. @make_method('/carla-control/exit', '')
  290. def set_exit_callback(self, path, args):
  291. print(path, args)
  292. self.host.QuitCallback.emit()
  293. @make_method(None, None)
  294. def fallback(self, path, args):
  295. print("ControlServer::fallback(\"%s\") - unknown message, args =" % path, args)
  296. # ------------------------------------------------------------------------------------------------------------
  297. # Main Window
  298. class HostWindowOSC(HostWindow):
  299. def __init__(self, host, oscAddr):
  300. HostWindow.__init__(self, host, False)
  301. self.host = host
  302. if False:
  303. # kdevelop likes this :)
  304. host = CarlaHostPlugin()
  305. self.host = host
  306. # ----------------------------------------------------------------------------------------------------
  307. # Internal stuff
  308. self.fOscAddress = oscAddr
  309. self.fOscServer = None
  310. # ----------------------------------------------------------------------------------------------------
  311. # Set up GUI (not connected)
  312. self.ui.act_file_refresh.setEnabled(False)
  313. # ----------------------------------------------------------------------------------------------------
  314. # Connect actions to functions
  315. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  316. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  317. # ----------------------------------------------------------------------------------------------------
  318. # Final setup
  319. if oscAddr:
  320. QTimer.singleShot(0, self.connectOsc)
  321. def connectOsc(self, addr = None):
  322. global lo_target, lo_target_name
  323. if addr is not None:
  324. self.fOscAddress = addr
  325. lo_target = Address(self.fOscAddress)
  326. lo_target_name = self.fOscAddress.rsplit("/", 1)[-1]
  327. print("Connecting to \"%s\" as '%s'..." % (self.fOscAddress, lo_target_name))
  328. try:
  329. self.fOscServer = CarlaControlServerThread(self.host, LO_UDP if self.fOscAddress.startswith("osc.udp") else LO_TCP)
  330. except: # ServerError as err:
  331. QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to connect, operation failed."))
  332. return
  333. self.fOscServer.start()
  334. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  335. self.startTimers()
  336. self.ui.act_file_refresh.setEnabled(True)
  337. def disconnectOsc(self):
  338. global lo_target, lo_target_name
  339. self.killTimers()
  340. self.ui.act_file_refresh.setEnabled(False)
  341. if lo_target is not None:
  342. lo_send(lo_target, "/unregister")
  343. if self.fOscServer is not None:
  344. self.fOscServer.stop()
  345. self.fOscServer = None
  346. self.removeAllPlugins()
  347. lo_target = None
  348. lo_target_name = ""
  349. self.fOscAddress = ""
  350. def removeAllPlugins(self):
  351. self.host.fPluginsInfo = []
  352. HostWindow.removeAllPlugins(self)
  353. @pyqtSlot()
  354. def slot_fileConnect(self):
  355. global lo_target, lo_target_name
  356. if lo_target and self.fOscServer:
  357. urlText = self.fOscAddress
  358. else:
  359. urlText = "osc.tcp://127.0.0.1:19000/Carla"
  360. addr, ok = QInputDialog.getText(self, self.tr("Carla Control - Connect"), self.tr("Address"), text=urlText)
  361. if not ok:
  362. return
  363. self.disconnectOsc()
  364. self.connectOsc(addr)
  365. @pyqtSlot()
  366. def slot_fileRefresh(self):
  367. global lo_target
  368. if lo_target is None or self.fOscServer is None:
  369. return
  370. self.killTimers()
  371. lo_send(lo_target, "/unregister")
  372. self.removeAllPlugins()
  373. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  374. self.startTimers()
  375. @pyqtSlot()
  376. def slot_handleQuitCallback(self):
  377. self.disconnectOsc()
  378. def closeEvent(self, event):
  379. global lo_target
  380. self.killTimers()
  381. if lo_target is not None and self.fOscServer is not None:
  382. lo_send(lo_target, "/unregister")
  383. HostWindow.closeEvent(self, event)
  384. # ------------------------------------------------------------------------------------------------------------