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.

672 lines
23KB

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