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.

607 lines
21KB

  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 (
  23. Address,
  24. AddressError,
  25. ServerError,
  26. Server,
  27. make_method,
  28. send as lo_send,
  29. TCP as LO_TCP,
  30. UDP as LO_UDP,
  31. )
  32. from random import random
  33. # ------------------------------------------------------------------------------------------------------------
  34. # Host OSC object
  35. class CarlaHostOSC(CarlaHostQtPlugin):
  36. def __init__(self):
  37. CarlaHostQtPlugin.__init__(self)
  38. self.lo_server_tcp = None
  39. self.lo_server_udp = None
  40. self.lo_target_tcp = None
  41. self.lo_target_udp = None
  42. self.lo_target_tcp_name = ""
  43. self.lo_target_udp_name = ""
  44. # -------------------------------------------------------------------
  45. def printAndReturnError(self, error):
  46. print(error)
  47. self.fLastError = error
  48. return False
  49. def sendMsg(self, lines):
  50. if len(lines) < 1:
  51. return self.printAndReturnError("not enough arguments")
  52. method = lines.pop(0)
  53. if method == "set_engine_option":
  54. print(method, lines)
  55. return True
  56. if self.lo_target_tcp is None:
  57. return self.printAndReturnError("lo_target_tcp is None")
  58. if self.lo_target_tcp_name is None:
  59. return self.printAndReturnError("lo_target_tcp_name is None")
  60. if method in ("clear_engine_xruns",
  61. "cancel_engine_action",
  62. #"load_file",
  63. #"load_project",
  64. #"save_project",
  65. #"clear_project_filename",
  66. "patchbay_connect",
  67. "patchbay_disconnect",
  68. "patchbay_refresh",
  69. "transport_play",
  70. "transport_pause",
  71. "transport_bpm",
  72. "transport_relocate",
  73. "add_plugin",
  74. "remove_plugin",
  75. "remove_all_plugins",
  76. "rename_plugin",
  77. "clone_plugin",
  78. "replace_plugin",
  79. "switch_plugins",
  80. #"load_plugin_state",
  81. #"save_plugin_state",
  82. ):
  83. path = "/ctrl/" + method
  84. elif method in (#"set_option",
  85. "set_active",
  86. "set_drywet",
  87. "set_volume",
  88. "set_balance_left",
  89. "set_balance_right",
  90. "set_panning",
  91. #"set_ctrl_channel",
  92. "set_parameter_value",
  93. "set_parameter_midi_channel",
  94. "set_parameter_midi_cc",
  95. "set_program",
  96. "set_midi_program",
  97. #"set_custom_data",
  98. #"set_chunk_data",
  99. #"prepare_for_save",
  100. #"reset_parameters",
  101. #"randomize_parameters",
  102. ):
  103. pluginId = lines.pop(0)
  104. path = "/%s/%i/%s" % (self.lo_target_tcp_name, pluginId, method)
  105. elif method == "send_midi_note":
  106. pluginId = lines.pop(0)
  107. channel, note, velocity = lines
  108. if velocity:
  109. path = "/%s/%i/note_on" % (self.lo_target_tcp_name, pluginId)
  110. else:
  111. path = "/%s/%i/note_off" % (self.lo_target_tcp_name, pluginId)
  112. lines.pop(2)
  113. else:
  114. return self.printAndReturnError("invalid method '%s'" % method)
  115. args = [int(line) if isinstance(line, bool) else line for line in lines]
  116. #print(path, args)
  117. lo_send(self.lo_target_tcp, path, *args)
  118. return True
  119. # -------------------------------------------------------------------
  120. def engine_init(self, driverName, clientName):
  121. print("engine_init", self.lo_target_tcp is not None)
  122. return self.lo_target_tcp is not None
  123. def engine_close(self):
  124. print("engine_close")
  125. return True
  126. def engine_idle(self):
  127. return
  128. def is_engine_running(self):
  129. return self.lo_target_tcp is not None
  130. def set_engine_about_to_close(self):
  131. return
  132. # ---------------------------------------------------------------------------------------------------------------------
  133. # OSC Control server
  134. class CarlaControlServerTCP(Server):
  135. def __init__(self, host):
  136. Server.__init__(self, proto=LO_TCP)
  137. if False:
  138. host = CarlaHostPlugin()
  139. self.host = host
  140. def idle(self):
  141. self.fReceivedMsgs = False
  142. while self.recv(0) and self.fReceivedMsgs:
  143. pass
  144. def getFullURL(self):
  145. return "%sctrl" % self.get_url()
  146. @make_method('/ctrl/cb', 'iiiiifs')
  147. def carla_cb(self, path, args):
  148. self.fReceivedMsgs = True
  149. action, pluginId, value1, value2, value3, valuef, valueStr = args
  150. self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
  151. engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
  152. @make_method('/ctrl/info', 'iiiihiisssssss')
  153. def carla_info(self, path, args):
  154. self.fReceivedMsgs = True
  155. (
  156. pluginId, type_, category, hints, uniqueId, optsAvail, optsEnabled,
  157. name, filename, iconName, realName, label, maker, copyright,
  158. ) = args
  159. hints &= ~PLUGIN_HAS_CUSTOM_UI
  160. pinfo = {
  161. 'type': type_,
  162. 'category': category,
  163. 'hints': hints,
  164. 'optionsAvailable': optsAvail,
  165. 'optionsEnabled': optsEnabled,
  166. 'uniqueId': uniqueId,
  167. 'filename': filename,
  168. 'name': name,
  169. 'label': label,
  170. 'maker': maker,
  171. 'copyright': copyright,
  172. 'iconName': iconName
  173. }
  174. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  175. self.host._set_pluginRealName(pluginId, realName)
  176. @make_method('/ctrl/ports', 'iiiiiiii')
  177. def carla_ports(self, path, args):
  178. self.fReceivedMsgs = True
  179. pluginId, audioIns, audioOuts, midiIns, midiOuts, paramIns, paramOuts, paramTotal = args
  180. self.host._set_audioCountInfo(pluginId, {'ins': audioIns, 'outs': audioOuts})
  181. self.host._set_midiCountInfo(pluginId, {'ins': midiOuts, 'outs': midiOuts})
  182. self.host._set_parameterCountInfo(pluginId, paramTotal, {'ins': paramIns, 'outs': paramOuts})
  183. @make_method('/ctrl/param', 'iiiiiissfffffff')
  184. def carla_param(self, path, args):
  185. self.fReceivedMsgs = True
  186. (
  187. pluginId, paramId, type_, hints, midiChan, midiCC, name, unit,
  188. def_, min_, max_, step, stepSmall, stepLarge, value
  189. ) = 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': midiCC,
  204. 'midiChannel': midiChan,
  205. }
  206. self.host._set_parameterData(pluginId, paramId, paramData)
  207. paramRanges = {
  208. 'def': def_,
  209. 'min': min_,
  210. 'max': max_,
  211. 'step': step,
  212. 'stepSmall': stepSmall,
  213. 'stepLarge': stepLarge,
  214. }
  215. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  216. self.host._set_parameterValue(pluginId, paramId, value)
  217. @make_method('/ctrl/count', 'iiiiii')
  218. def carla_count(self, path, args):
  219. self.fReceivedMsgs = True
  220. pluginId, pcount, mpcount, cdcount, cp, cmp = args
  221. self.host._set_programCount(pluginId, pcount)
  222. self.host._set_midiProgramCount(pluginId, mpcount)
  223. self.host._set_customDataCount(pluginId, cdcount)
  224. self.host._set_pluginInfoUpdate(pluginId, { 'programCurrent': cp, 'midiProgramCurrent': cmp })
  225. @make_method('/ctrl/pcount', 'iii')
  226. def carla_pcount(self, path, args):
  227. self.fReceivedMsgs = True
  228. pluginId, pcount, mpcount = args
  229. self.host._set_programCount(pluginId, pcount)
  230. self.host._set_midiProgramCount(pluginId, mpcount)
  231. @make_method('/ctrl/prog', 'iis')
  232. def carla_prog(self, path, args):
  233. self.fReceivedMsgs = True
  234. pluginId, progId, progName = args
  235. self.host._set_programName(pluginId, progId, progName)
  236. @make_method('/ctrl/mprog', 'iiiis')
  237. def carla_mprog(self, path, args):
  238. self.fReceivedMsgs = True
  239. pluginId, midiProgId, bank, program, name = args
  240. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  241. @make_method('/ctrl/cdata', 'iisss')
  242. def carla_cdata(self, path, args):
  243. self.fReceivedMsgs = True
  244. pluginId, index, type_, key, value = args
  245. self.host._set_customData(pluginId, index, { 'type': type_, 'key': key, 'value': value })
  246. @make_method('/ctrl/iparams', 'ifffffff')
  247. def carla_iparams(self, path, args):
  248. self.fReceivedMsgs = True
  249. pluginId, active, drywet, volume, balLeft, balRight, pan, ctrlChan = args
  250. self.host._set_internalValue(pluginId, PARAMETER_ACTIVE, active)
  251. self.host._set_internalValue(pluginId, PARAMETER_DRYWET, drywet)
  252. self.host._set_internalValue(pluginId, PARAMETER_VOLUME, volume)
  253. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_LEFT, balLeft)
  254. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_RIGHT, balRight)
  255. self.host._set_internalValue(pluginId, PARAMETER_PANNING, pan)
  256. self.host._set_internalValue(pluginId, PARAMETER_CTRL_CHANNEL, ctrlChan)
  257. @make_method('/ctrl/exit', '')
  258. def carla_exit(self, path, args):
  259. self.fReceivedMsgs = True
  260. #self.host.lo_target_tcp = None
  261. self.host.QuitCallback.emit()
  262. @make_method('/ctrl/exit-error', 's')
  263. def carla_exit_error(self, path, args):
  264. self.fReceivedMsgs = True
  265. error, = args
  266. self.host.lo_target_tcp = None
  267. self.host.QuitCallback.emit()
  268. self.host.ErrorCallback.emit(error)
  269. @make_method(None, None)
  270. def fallback(self, path, args):
  271. print("ControlServerTCP::fallback(\"%s\") - unknown message, args =" % path, args)
  272. self.fReceivedMsgs = True
  273. # ---------------------------------------------------------------------------------------------------------------------
  274. class CarlaControlServerUDP(Server):
  275. def __init__(self, host):
  276. Server.__init__(self, proto=LO_UDP)
  277. if False:
  278. host = CarlaHostPlugin()
  279. self.host = host
  280. def idle(self):
  281. self.fReceivedMsgs = False
  282. while self.recv(0) and self.fReceivedMsgs:
  283. pass
  284. def getFullURL(self):
  285. return "%sctrl" % self.get_url()
  286. @make_method('/ctrl/runtime', 'fiihiiif')
  287. def carla_runtime(self, path, args):
  288. self.fReceivedMsgs = True
  289. load, xruns, playing, frame, bar, beat, tick, bpm = args
  290. self.host._set_runtime_info(load, xruns)
  291. self.host._set_transport(bool(playing), frame, bar, beat, tick, bpm)
  292. @make_method('/ctrl/param', 'iif')
  293. def carla_param_fixme(self, path, args):
  294. self.fReceivedMsgs = True
  295. pluginId, paramId, paramValue = args
  296. self.host._set_parameterValue(pluginId, paramId, paramValue)
  297. @make_method('/ctrl/peaks', 'iffff')
  298. def carla_peaks(self, path, args):
  299. self.fReceivedMsgs = True
  300. pluginId, in1, in2, out1, out2 = args
  301. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  302. @make_method(None, None)
  303. def fallback(self, path, args):
  304. print("ControlServerUDP::fallback(\"%s\") - unknown message, args =" % path, args)
  305. self.fReceivedMsgs = True
  306. # ---------------------------------------------------------------------------------------------------------------------
  307. # Main Window
  308. class HostWindowOSC(HostWindow):
  309. def __init__(self, host, oscAddr):
  310. HostWindow.__init__(self, host, True)
  311. self.host = host
  312. if False:
  313. # kdevelop likes this :)
  314. host = CarlaHostPlugin()
  315. self.host = host
  316. # ----------------------------------------------------------------------------------------------------
  317. # Connect actions to functions
  318. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  319. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  320. # ----------------------------------------------------------------------------------------------------
  321. # Final setup
  322. if oscAddr:
  323. QTimer.singleShot(0, self.connectOsc)
  324. def connectOsc(self, addrTCP = None, addrUDP = None):
  325. if addrTCP is not None:
  326. self.fOscAddressTCP = addrTCP
  327. if addrUDP is not None:
  328. self.fOscAddressUDP = addrUDP
  329. lo_target_tcp_name = self.fOscAddressTCP.rsplit("/", 1)[-1]
  330. lo_target_udp_name = self.fOscAddressUDP.rsplit("/", 1)[-1]
  331. err = None
  332. try:
  333. lo_target_tcp = Address(self.fOscAddressTCP)
  334. lo_server_tcp = CarlaControlServerTCP(self.host)
  335. lo_send(lo_target_tcp, "/register", lo_server_tcp.getFullURL())
  336. print(lo_server_tcp.getFullURL())
  337. lo_target_udp = Address(self.fOscAddressUDP)
  338. lo_server_udp = CarlaControlServerUDP(self.host)
  339. lo_send(lo_target_udp, "/register", lo_server_udp.getFullURL())
  340. print(lo_server_udp.getFullURL())
  341. except AddressError as e:
  342. err = e
  343. except OSError as e:
  344. err = e
  345. except:
  346. err = Exception()
  347. if err is not None:
  348. fullError = self.tr("Failed to connect to the Carla instance.")
  349. if len(err.args) > 0:
  350. fullError += " %s\n%s\n" % (self.tr("Error was:"), err.args[0])
  351. fullError += "\n"
  352. fullError += self.tr("Make sure the remote Carla is running and the URL and Port are correct.") + "\n"
  353. fullError += self.tr("If it still does not work, check your current device and the remote's firewall.")
  354. CustomMessageBox(self,
  355. QMessageBox.Warning,
  356. self.tr("Error"),
  357. self.tr("Connection failed"),
  358. fullError,
  359. QMessageBox.Ok,
  360. QMessageBox.Ok)
  361. return
  362. self.host.lo_server_tcp = lo_server_tcp
  363. self.host.lo_target_tcp = lo_target_tcp
  364. self.host.lo_target_tcp_name = lo_target_tcp_name
  365. self.host.lo_server_udp = lo_server_udp
  366. self.host.lo_target_udp = lo_target_udp
  367. self.host.lo_target_udp_name = lo_target_udp_name
  368. self.ui.act_file_refresh.setEnabled(True)
  369. self.startTimers()
  370. def disconnectOsc(self):
  371. self.killTimers()
  372. self.unregister()
  373. self.removeAllPlugins()
  374. patchcanvas.clear()
  375. self.ui.act_file_refresh.setEnabled(False)
  376. # --------------------------------------------------------------------------------------------------------
  377. def unregister(self):
  378. if self.host.lo_server_tcp is not None:
  379. if self.host.lo_target_tcp is not None:
  380. try:
  381. lo_send(self.host.lo_target_tcp, "/unregister", self.host.lo_server_tcp.getFullURL())
  382. except:
  383. pass
  384. self.host.lo_target_tcp = None
  385. while self.host.lo_server_tcp.recv(0):
  386. pass
  387. #self.host.lo_server_tcp.free()
  388. self.host.lo_server_tcp = None
  389. if self.host.lo_server_udp is not None:
  390. if self.host.lo_target_udp is not None:
  391. try:
  392. lo_send(self.host.lo_target_udp, "/unregister", self.host.lo_server_udp.getFullURL())
  393. except:
  394. pass
  395. self.host.lo_target_udp = None
  396. while self.host.lo_server_udp.recv(0):
  397. pass
  398. #self.host.lo_server_udp.free()
  399. self.host.lo_server_udp = None
  400. self.host.lo_target_tcp_name = ""
  401. self.host.lo_target_udp_name = ""
  402. # --------------------------------------------------------------------------------------------------------
  403. # Timers
  404. def idleFast(self):
  405. HostWindow.idleFast(self)
  406. if self.host.lo_server_tcp is not None:
  407. self.host.lo_server_tcp.idle()
  408. else:
  409. self.disconnectOsc()
  410. if self.host.lo_server_udp is not None:
  411. self.host.lo_server_udp.idle()
  412. else:
  413. self.disconnectOsc()
  414. # --------------------------------------------------------------------------------------------------------
  415. def removeAllPlugins(self):
  416. self.host.fPluginsInfo = []
  417. HostWindow.removeAllPlugins(self)
  418. # --------------------------------------------------------------------------------------------------------
  419. def loadSettings(self, firstTime):
  420. settings = HostWindow.loadSettings(self, firstTime)
  421. self.fOscAddressTCP = settings.value("RemoteAddressTCP", "osc.tcp://127.0.0.1:22752/Carla", type=str)
  422. self.fOscAddressUDP = settings.value("RemoteAddressUDP", "osc.udp://127.0.0.1:22752/Carla", type=str)
  423. def saveSettings(self):
  424. settings = HostWindow.saveSettings(self)
  425. if self.fOscAddressTCP:
  426. settings.setValue("RemoteAddressTCP", self.fOscAddressTCP)
  427. if self.fOscAddressUDP:
  428. settings.setValue("RemoteAddressUDP", self.fOscAddressUDP)
  429. # --------------------------------------------------------------------------------------------------------
  430. @pyqtSlot()
  431. def slot_fileConnect(self):
  432. dialog = QInputDialog(self)
  433. dialog.setInputMode(QInputDialog.TextInput)
  434. dialog.setLabelText(self.tr("Address:"))
  435. dialog.setTextValue(self.fOscAddressTCP or "osc.tcp://127.0.0.1:22752/Carla")
  436. dialog.setWindowTitle(self.tr("Carla Control - Connect"))
  437. dialog.resize(400,1)
  438. ok = dialog.exec_()
  439. addr = dialog.textValue().strip()
  440. del dialog
  441. if not ok:
  442. return
  443. if not addr:
  444. return
  445. self.disconnectOsc()
  446. if addr:
  447. self.connectOsc(addr.replace("osc.udp:", "osc.tcp:"), addr.replace("osc.tcp:", "osc.udp:"))
  448. @pyqtSlot()
  449. def slot_fileRefresh(self):
  450. if None in (self.host.lo_server_tcp, self.host.lo_server_udp, self.host.lo_target_tcp, self.host.lo_target_udp):
  451. return
  452. lo_send(self.host.lo_target_udp, "/unregister", self.host.lo_server_udp.getFullURL())
  453. while self.host.lo_server_udp.recv(0):
  454. pass
  455. #self.host.lo_server_udp.free()
  456. lo_send(self.host.lo_target_tcp, "/unregister", self.host.lo_server_tcp.getFullURL())
  457. while self.host.lo_server_tcp.recv(0):
  458. pass
  459. #self.host.lo_server_tcp.free()
  460. self.removeAllPlugins()
  461. patchcanvas.clear()
  462. self.host.lo_server_tcp = CarlaControlServerTCP(self.host)
  463. self.host.lo_server_udp = CarlaControlServerUDP(self.host)
  464. try:
  465. lo_send(self.host.lo_target_tcp, "/register", self.host.lo_server_tcp.getFullURL())
  466. except:
  467. self.disconnectOsc()
  468. return
  469. try:
  470. lo_send(self.host.lo_target_udp, "/register", self.host.lo_server_udp.getFullURL())
  471. except:
  472. self.disconnectOsc()
  473. return
  474. @pyqtSlot()
  475. def slot_handleQuitCallback(self):
  476. self.disconnectOsc()
  477. HostWindow.slot_handleQuitCallback(self)
  478. # --------------------------------------------------------------------------------------------------------
  479. def closeEvent(self, event):
  480. self.killTimers()
  481. self.unregister()
  482. HostWindow.closeEvent(self, event)
  483. # ------------------------------------------------------------------------------------------------------------