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 18KB

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
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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, Server
  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 CarlaControlServer(Server):
  109. def __init__(self, host, mode):
  110. Server.__init__(self, 8998 + int(random()*9000), mode)
  111. self.host = host
  112. def idle(self):
  113. self.fReceivedMsgs = False
  114. while self.recv(0) and self.fReceivedMsgs:
  115. pass
  116. def getFullURL(self):
  117. return "%scarla-control" % self.get_url()
  118. @make_method('/carla-control/add_plugin_start', 'is') # FIXME skip name
  119. def add_plugin_start_callback(self, path, args):
  120. print(path, args)
  121. self.fReceivedMsgs = True
  122. pluginId, pluginName = args
  123. self.host._add(pluginId)
  124. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  125. @make_method('/carla-control/add_plugin_end', 'i') # FIXME skip name
  126. def add_plugin_end_callback(self, path, args):
  127. print(path, args)
  128. self.fReceivedMsgs = True
  129. pluginId, = args
  130. self.host.PluginAddedCallback.emit(pluginId, "") #self.fPluginsInfo[pluginId].pluginInfo['name'])
  131. @make_method('/carla-control/remove_plugin', 'i')
  132. def remove_plugin_callback(self, path, args):
  133. print(path, args)
  134. self.fReceivedMsgs = True
  135. pluginId, = args
  136. self.host.PluginRemovedCallback.emit(pluginId)
  137. @make_method('/carla-control/set_plugin_info1', 'iiiih')
  138. def set_plugin_info1_callback(self, path, args):
  139. print(path, args)
  140. self.fReceivedMsgs = True
  141. pluginId, type_, category, hints, uniqueId = args # , optsAvail, optsEnabled
  142. optsAvail = optsEnabled = 0x0 # FIXME
  143. hints &= ~PLUGIN_HAS_CUSTOM_UI
  144. pinfo = {
  145. 'type': type_,
  146. 'category': category,
  147. 'hints': hints,
  148. 'optionsAvailable': optsAvail,
  149. 'optionsEnabled': optsEnabled,
  150. 'uniqueId': uniqueId
  151. }
  152. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  153. @make_method('/carla-control/set_plugin_info2', 'issss')
  154. def set_plugin_info2_callback(self, path, args):
  155. print(path, args)
  156. self.fReceivedMsgs = True
  157. pluginId, realName, label, maker, copyright = args # , filename, name, iconName
  158. filename = name = iconName = "" # FIXME
  159. pinfo = {
  160. 'filename': filename,
  161. #'name': name, # FIXME
  162. 'label': label,
  163. 'maker': maker,
  164. 'copyright': copyright,
  165. 'iconName': iconName
  166. }
  167. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  168. self.host._set_pluginRealName(pluginId, realName)
  169. @make_method('/carla-control/set_audio_count', 'iii')
  170. def set_audio_count_callback(self, path, args):
  171. print(path, args)
  172. self.fReceivedMsgs = True
  173. pluginId, ins, outs = args
  174. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  175. @make_method('/carla-control/set_midi_count', 'iii')
  176. def set_midi_count_callback(self, path, args):
  177. print(path, args)
  178. self.fReceivedMsgs = True
  179. pluginId, ins, outs = args
  180. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  181. @make_method('/carla-control/set_parameter_count', 'iii') # FIXME
  182. def set_parameter_count_callback(self, path, args):
  183. print(path, args)
  184. self.fReceivedMsgs = True
  185. pluginId, ins, outs = args # , count
  186. count = ins + outs
  187. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  188. @make_method('/carla-control/set_program_count', 'ii')
  189. def set_program_count_callback(self, path, args):
  190. print(path, args)
  191. self.fReceivedMsgs = True
  192. pluginId, count = args
  193. self.host._set_programCount(pluginId, count)
  194. @make_method('/carla-control/set_midi_program_count', 'ii')
  195. def set_midi_program_count_callback(self, path, args):
  196. print(path, args)
  197. self.fReceivedMsgs = True
  198. pluginId, count = args
  199. self.host._set_midiProgramCount(pluginId, count)
  200. @make_method('/carla-control/set_parameter_data', 'iiiiss')
  201. def set_parameter_data_callback(self, path, args):
  202. print(path, args)
  203. self.fReceivedMsgs = True
  204. pluginId, paramId, type_, hints, name, unit = args
  205. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  206. paramInfo = {
  207. 'name': name,
  208. 'symbol': "",
  209. 'unit': unit,
  210. 'scalePointCount': 0,
  211. }
  212. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  213. paramData = {
  214. 'type': type_,
  215. 'hints': hints,
  216. 'index': paramId,
  217. 'rindex': -1,
  218. 'midiCC': -1,
  219. 'midiChannel': 0
  220. }
  221. self.host._set_parameterData(pluginId, paramId, paramData)
  222. @make_method('/carla-control/set_parameter_ranges1', 'iifff')
  223. def set_parameter_ranges1_callback(self, path, args):
  224. print(path, args)
  225. self.fReceivedMsgs = True
  226. pluginId, paramId, def_, min_, max_ = args
  227. paramRanges = {
  228. 'def': def_,
  229. 'min': min_,
  230. 'max': max_
  231. }
  232. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  233. @make_method('/carla-control/set_parameter_ranges2', 'iifff')
  234. def set_parameter_ranges2_callback(self, path, args):
  235. print(path, args)
  236. self.fReceivedMsgs = True
  237. pluginId, paramId, step, stepSmall, stepLarge = args
  238. paramRanges = {
  239. 'step': step,
  240. 'stepSmall': stepSmall,
  241. 'stepLarge': stepLarge
  242. }
  243. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  244. @make_method('/carla-control/set_parameter_midi_cc', 'iii')
  245. def set_parameter_midi_cc_callback(self, path, args):
  246. print(path, args)
  247. self.fReceivedMsgs = True
  248. pluginId, paramId, cc = args
  249. self.host._set_parameterMidiCC(pluginId, paramId, cc)
  250. self.host.ParameterMidiCcChangedCallback.emit(pluginId, paramId, cc)
  251. @make_method('/carla-control/set_parameter_midi_channel', 'iii')
  252. def set_parameter_midi_channel_callback(self, path, args):
  253. print(path, args)
  254. self.fReceivedMsgs = True
  255. pluginId, paramId, channel = args
  256. self.host._set_parameterMidiChannel(pluginId, paramId, channel)
  257. self.host.ParameterMidiChannelChangedCallback.emit(pluginId, paramId, channel)
  258. @make_method('/carla-control/set_parameter_value', 'iif')
  259. def set_parameter_value_callback(self, path, args):
  260. pluginId, paramId, paramValue = args
  261. if paramId < 0:
  262. self.host._set_internalValue(pluginId, paramId, paramValue)
  263. else:
  264. self.host._set_parameterValue(pluginId, paramId, paramValue)
  265. self.host.ParameterValueChangedCallback.emit(pluginId, paramId, paramValue)
  266. @make_method('/carla-control/set_default_value', 'iif')
  267. def set_default_value_callback(self, path, args):
  268. print(path, args)
  269. self.fReceivedMsgs = True
  270. pluginId, paramId, paramValue = args
  271. self.host._set_parameterDefault(pluginId, paramId, paramValue)
  272. self.host.ParameterDefaultChangedCallback.emit(pluginId, paramId, paramValue)
  273. @make_method('/carla-control/set_current_program', 'ii')
  274. def set_current_program_callback(self, path, args):
  275. print(path, args)
  276. self.fReceivedMsgs = True
  277. pluginId, current = args
  278. self.host._set_currentProgram(pluginId, current)
  279. self.host.ProgramChangedCallback.emit(pluginId, current)
  280. @make_method('/carla-control/set_current_midi_program', 'ii')
  281. def set_current_midi_program_callback(self, path, args):
  282. print(path, args)
  283. self.fReceivedMsgs = True
  284. pluginId, current = args
  285. self.host._set_currentMidiProgram(pluginId, current)
  286. self.host.MidiProgramChangedCallback.emit(pluginId, current)
  287. @make_method('/carla-control/set_program_name', 'iis')
  288. def set_program_name_callback(self, path, args):
  289. print(path, args)
  290. self.fReceivedMsgs = True
  291. pluginId, progId, progName = args
  292. self.host._set_programName(pluginId, progId, progName)
  293. @make_method('/carla-control/set_midi_program_data', 'iiiis')
  294. def set_midi_program_data_callback(self, path, args):
  295. print(path, args)
  296. self.fReceivedMsgs = True
  297. pluginId, midiProgId, bank, program, name = args
  298. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  299. @make_method('/carla-control/note_on', 'iiii')
  300. def set_note_on_callback(self, path, args):
  301. print(path, args)
  302. self.fReceivedMsgs = True
  303. pluginId, channel, note, velocity = args
  304. self.host.NoteOnCallback.emit(pluginId, channel, note, velocity)
  305. @make_method('/carla-control/note_off', 'iii')
  306. def set_note_off_callback(self, path, args):
  307. print(path, args)
  308. self.fReceivedMsgs = True
  309. pluginId, channel, note = args
  310. self.host.NoteOffCallback.emit(pluginId, channel, note)
  311. @make_method('/carla-control/set_peaks', 'iffff')
  312. def set_peaks_callback(self, path, args):
  313. self.fReceivedMsgs = True
  314. pluginId, in1, in2, out1, out2 = args
  315. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  316. @make_method('/carla-control/exit', '')
  317. def set_exit_callback(self, path, args):
  318. print(path, args)
  319. self.fReceivedMsgs = True
  320. self.host.QuitCallback.emit()
  321. @make_method(None, None)
  322. def fallback(self, path, args):
  323. print("ControlServer::fallback(\"%s\") - unknown message, args =" % path, args)
  324. self.fReceivedMsgs = True
  325. # ------------------------------------------------------------------------------------------------------------
  326. # Main Window
  327. class HostWindowOSC(HostWindow):
  328. def __init__(self, host, oscAddr):
  329. HostWindow.__init__(self, host, False)
  330. self.host = host
  331. if False:
  332. # kdevelop likes this :)
  333. host = CarlaHostPlugin()
  334. self.host = host
  335. # ----------------------------------------------------------------------------------------------------
  336. # Internal stuff
  337. self.fIdleTimer = 0
  338. self.fOscAddress = oscAddr
  339. self.fOscServer = None
  340. # ----------------------------------------------------------------------------------------------------
  341. # Set up GUI (not connected)
  342. self.ui.act_file_refresh.setEnabled(False)
  343. # ----------------------------------------------------------------------------------------------------
  344. # Connect actions to functions
  345. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  346. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  347. # ----------------------------------------------------------------------------------------------------
  348. # Final setup
  349. if oscAddr:
  350. QTimer.singleShot(0, self.connectOsc)
  351. def connectOsc(self, addr = None):
  352. global lo_target, lo_target_name
  353. if addr is not None:
  354. self.fOscAddress = addr
  355. lo_target = Address(self.fOscAddress)
  356. lo_target_name = self.fOscAddress.rsplit("/", 1)[-1]
  357. print("Connecting to \"%s\" as '%s'..." % (self.fOscAddress, lo_target_name))
  358. try:
  359. self.fOscServer = CarlaControlServer(self.host, LO_UDP if self.fOscAddress.startswith("osc.udp") else LO_TCP)
  360. except: # ServerError as err:
  361. QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to connect, operation failed."))
  362. return
  363. self.fIdleTimer = self.startTimer(20)
  364. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  365. self.startTimers()
  366. self.ui.act_file_refresh.setEnabled(True)
  367. def disconnectOsc(self):
  368. global lo_target, lo_target_name
  369. self.killTimers()
  370. self.ui.act_file_refresh.setEnabled(False)
  371. if lo_target is not None:
  372. try:
  373. lo_send(lo_target, "/unregister")
  374. except:
  375. pass
  376. self.killTimer(self.fIdleTimer)
  377. self.fIdleTimer = 0
  378. if self.fOscServer is not None:
  379. del self.fOscServer
  380. self.fOscServer = None
  381. self.removeAllPlugins()
  382. lo_target = None
  383. lo_target_name = ""
  384. self.fOscAddress = ""
  385. def removeAllPlugins(self):
  386. self.host.fPluginsInfo = []
  387. HostWindow.removeAllPlugins(self)
  388. @pyqtSlot()
  389. def slot_fileConnect(self):
  390. global lo_target, lo_target_name
  391. if lo_target and self.fOscServer:
  392. urlText = self.fOscAddress
  393. else:
  394. urlText = "osc.tcp://127.0.0.1:19000/Carla"
  395. addr, ok = QInputDialog.getText(self, self.tr("Carla Control - Connect"), self.tr("Address"), text=urlText)
  396. if not ok:
  397. return
  398. self.disconnectOsc()
  399. self.connectOsc(addr)
  400. @pyqtSlot()
  401. def slot_fileRefresh(self):
  402. global lo_target
  403. if lo_target is None or self.fOscServer is None:
  404. return
  405. self.killTimers()
  406. lo_send(lo_target, "/unregister")
  407. self.removeAllPlugins()
  408. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  409. self.startTimers()
  410. @pyqtSlot()
  411. def slot_handleQuitCallback(self):
  412. self.disconnectOsc()
  413. def timerEvent(self, event):
  414. if event.timerId() == self.fIdleTimer:
  415. self.fOscServer.idle()
  416. HostWindow.timerEvent(self, event)
  417. def closeEvent(self, event):
  418. global lo_target
  419. self.killTimers()
  420. if lo_target is not None and self.fOscServer is not None:
  421. lo_send(lo_target, "/unregister")
  422. HostWindow.closeEvent(self, event)
  423. # ------------------------------------------------------------------------------------------------------------