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.

655 lines
22KB

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