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.

594 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. "send_midi_note"
  103. ):
  104. pluginId = lines.pop(0)
  105. path = "/%s/%i/%s" % (self.lo_target_tcp_name, pluginId, method)
  106. else:
  107. return self.printAndReturnError("invalid method '%s'" % method)
  108. args = [int(line) if isinstance(line, bool) else line for line in lines]
  109. #print(path, args)
  110. lo_send(self.lo_target_tcp, path, *args)
  111. return True
  112. # -------------------------------------------------------------------
  113. def engine_init(self, driverName, clientName):
  114. print("engine_init", self.lo_target_tcp is not None)
  115. return self.lo_target_tcp is not None
  116. def engine_close(self):
  117. print("engine_close")
  118. return True
  119. def engine_idle(self):
  120. return
  121. def is_engine_running(self):
  122. return self.lo_target_tcp is not None
  123. def set_engine_about_to_close(self):
  124. return
  125. # ---------------------------------------------------------------------------------------------------------------------
  126. # OSC Control server
  127. class CarlaControlServerTCP(Server):
  128. def __init__(self, host):
  129. Server.__init__(self, proto=LO_TCP)
  130. self.host = host
  131. def idle(self):
  132. self.fReceivedMsgs = False
  133. while self.recv(0) and self.fReceivedMsgs:
  134. pass
  135. def getFullURL(self):
  136. return "%sctrl" % self.get_url()
  137. @make_method('/ctrl/cb', 'iiiiifs')
  138. def carla_cb(self, path, args):
  139. self.fReceivedMsgs = True
  140. action, pluginId, value1, value2, value3, valuef, valueStr = args
  141. self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
  142. engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
  143. @make_method('/ctrl/init', 'is') # FIXME set name in add method
  144. def carla_init(self, path, args):
  145. self.fReceivedMsgs = True
  146. pluginId, pluginName = args
  147. self.host._add(pluginId)
  148. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  149. @make_method('/ctrl/ports', 'iiiiiiii')
  150. def carla_ports(self, path, args):
  151. self.fReceivedMsgs = True
  152. pluginId, audioIns, audioOuts, midiIns, midiOuts, paramIns, paramOuts, paramTotal = args
  153. self.host._set_audioCountInfo(pluginId, {'ins': audioIns, 'outs': audioOuts})
  154. self.host._set_midiCountInfo(pluginId, {'ins': midiOuts, 'outs': midiOuts})
  155. self.host._set_parameterCountInfo(pluginId, paramTotal, {'ins': paramIns, 'outs': paramOuts})
  156. @make_method('/ctrl/info', 'iiiihssss')
  157. def carla_info(self, path, args):
  158. self.fReceivedMsgs = True
  159. pluginId, type_, category, hints, uniqueId, realName, label, maker, copyright = args
  160. optsAvail = optsEnabled = 0x0 # FIXME
  161. filename = name = iconName = "" # FIXME
  162. hints &= ~PLUGIN_HAS_CUSTOM_UI
  163. pinfo = {
  164. 'type': type_,
  165. 'category': category,
  166. 'hints': hints,
  167. 'optionsAvailable': optsAvail,
  168. 'optionsEnabled': optsEnabled,
  169. 'uniqueId': uniqueId,
  170. 'filename': filename,
  171. #'name': name, # FIXME
  172. 'label': label,
  173. 'maker': maker,
  174. 'copyright': copyright,
  175. 'iconName': iconName
  176. }
  177. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  178. self.host._set_pluginRealName(pluginId, realName)
  179. @make_method('/ctrl/param', 'iiiiiissfffffff')
  180. def carla_param(self, path, args):
  181. self.fReceivedMsgs = True
  182. (
  183. pluginId, paramId, type_, hints, midiChan, midiCC, name, unit,
  184. def_, min_, max_, step, stepSmall, stepLarge, value
  185. ) = args
  186. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  187. paramInfo = {
  188. 'name': name,
  189. 'symbol': "",
  190. 'unit': unit,
  191. 'scalePointCount': 0,
  192. }
  193. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  194. paramData = {
  195. 'type': type_,
  196. 'hints': hints,
  197. 'index': paramId,
  198. 'rindex': -1,
  199. 'midiCC': midiCC,
  200. 'midiChannel': midiChan,
  201. }
  202. self.host._set_parameterData(pluginId, paramId, paramData)
  203. paramRanges = {
  204. 'def': def_,
  205. 'min': min_,
  206. 'max': max_,
  207. 'step': step,
  208. 'stepSmall': stepSmall,
  209. 'stepLarge': stepLarge,
  210. }
  211. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  212. self.host._set_parameterValue(pluginId, paramId, value)
  213. @make_method('/ctrl/iparams', 'ifffffff')
  214. def carla_iparams(self, path, args):
  215. self.fReceivedMsgs = True
  216. pluginId, active, drywet, volume, balLeft, balRight, pan, ctrlChan = args
  217. self.host._set_internalValue(pluginId, PARAMETER_ACTIVE, active)
  218. self.host._set_internalValue(pluginId, PARAMETER_DRYWET, drywet)
  219. self.host._set_internalValue(pluginId, PARAMETER_VOLUME, volume)
  220. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_LEFT, balLeft)
  221. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_RIGHT, balRight)
  222. self.host._set_internalValue(pluginId, PARAMETER_PANNING, pan)
  223. self.host._set_internalValue(pluginId, PARAMETER_CTRL_CHANNEL, ctrlChan)
  224. #@make_method('/ctrl/set_program_count', 'ii')
  225. #def set_program_count_callback(self, path, args):
  226. #print(path, args)
  227. #self.fReceivedMsgs = True
  228. #pluginId, count = args
  229. #self.host._set_programCount(pluginId, count)
  230. #@make_method('/ctrl/set_midi_program_count', 'ii')
  231. #def set_midi_program_count_callback(self, path, args):
  232. #print(path, args)
  233. #self.fReceivedMsgs = True
  234. #pluginId, count = args
  235. #self.host._set_midiProgramCount(pluginId, count)
  236. #@make_method('/ctrl/set_program_name', 'iis')
  237. #def set_program_name_callback(self, path, args):
  238. #print(path, args)
  239. #self.fReceivedMsgs = True
  240. #pluginId, progId, progName = args
  241. #self.host._set_programName(pluginId, progId, progName)
  242. #@make_method('/ctrl/set_midi_program_data', 'iiiis')
  243. #def set_midi_program_data_callback(self, path, args):
  244. #print(path, args)
  245. #self.fReceivedMsgs = True
  246. #pluginId, midiProgId, bank, program, name = args
  247. #self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  248. #@make_method('/note_on', 'iiii')
  249. @make_method('/ctrl/exit', '')
  250. def carla_exit(self, path, args):
  251. self.fReceivedMsgs = True
  252. #self.host.lo_target_tcp = None
  253. self.host.QuitCallback.emit()
  254. @make_method('/ctrl/exit-error', 's')
  255. def carla_exit_error(self, path, args):
  256. self.fReceivedMsgs = True
  257. error, = args
  258. self.host.lo_target_tcp = None
  259. self.host.QuitCallback.emit()
  260. self.host.ErrorCallback.emit(error)
  261. @make_method(None, None)
  262. def fallback(self, path, args):
  263. print("ControlServerTCP::fallback(\"%s\") - unknown message, args =" % path, args)
  264. self.fReceivedMsgs = True
  265. # ---------------------------------------------------------------------------------------------------------------------
  266. class CarlaControlServerUDP(Server):
  267. def __init__(self, host):
  268. Server.__init__(self, proto=LO_UDP)
  269. self.host = host
  270. def idle(self):
  271. self.fReceivedMsgs = False
  272. while self.recv(0) and self.fReceivedMsgs:
  273. pass
  274. def getFullURL(self):
  275. return "%sctrl" % self.get_url()
  276. @make_method('/ctrl/runtime', 'fiihiiif')
  277. def carla_runtime(self, path, args):
  278. self.fReceivedMsgs = True
  279. load, xruns, playing, frame, bar, beat, tick, bpm = args
  280. self.host._set_runtime_info(load, xruns)
  281. self.host._set_transport(bool(playing), frame, bar, beat, tick, bpm)
  282. @make_method('/ctrl/param', 'iif')
  283. def carla_param_fixme(self, path, args):
  284. self.fReceivedMsgs = True
  285. pluginId, paramId, paramValue = args
  286. self.host._set_parameterValue(pluginId, paramId, paramValue)
  287. @make_method('/ctrl/peaks', 'iffff')
  288. def carla_peaks(self, path, args):
  289. self.fReceivedMsgs = True
  290. pluginId, in1, in2, out1, out2 = args
  291. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  292. @make_method(None, None)
  293. def fallback(self, path, args):
  294. print("ControlServerUDP::fallback(\"%s\") - unknown message, args =" % path, args)
  295. self.fReceivedMsgs = True
  296. # ---------------------------------------------------------------------------------------------------------------------
  297. # Main Window
  298. class HostWindowOSC(HostWindow):
  299. def __init__(self, host, oscAddr):
  300. HostWindow.__init__(self, host, True)
  301. self.host = host
  302. if False:
  303. # kdevelop likes this :)
  304. host = CarlaHostPlugin()
  305. self.host = host
  306. # ----------------------------------------------------------------------------------------------------
  307. # Connect actions to functions
  308. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  309. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  310. # ----------------------------------------------------------------------------------------------------
  311. # Final setup
  312. if oscAddr:
  313. QTimer.singleShot(0, self.connectOsc)
  314. def connectOsc(self, addrTCP = None, addrUDP = None):
  315. if addrTCP is not None:
  316. self.fOscAddressTCP = addrTCP
  317. if addrUDP is not None:
  318. self.fOscAddressUDP = addrUDP
  319. lo_target_tcp_name = self.fOscAddressTCP.rsplit("/", 1)[-1]
  320. lo_target_udp_name = self.fOscAddressUDP.rsplit("/", 1)[-1]
  321. err = None
  322. try:
  323. lo_target_tcp = Address(self.fOscAddressTCP)
  324. lo_server_tcp = CarlaControlServerTCP(self.host)
  325. lo_send(lo_target_tcp, "/register", lo_server_tcp.getFullURL())
  326. print(lo_server_tcp.getFullURL())
  327. lo_target_udp = Address(self.fOscAddressUDP)
  328. lo_server_udp = CarlaControlServerUDP(self.host)
  329. lo_send(lo_target_udp, "/register", lo_server_udp.getFullURL())
  330. print(lo_server_udp.getFullURL())
  331. except AddressError as e:
  332. err = e
  333. except OSError as e:
  334. err = e
  335. except:
  336. err = Exception()
  337. if err is not None:
  338. fullError = self.tr("Failed to connect to the Carla instance.")
  339. if len(err.args) > 0:
  340. fullError += " %s\n%s\n" % (self.tr("Error was:"), err.args[0])
  341. fullError += "\n"
  342. fullError += self.tr("Make sure the remote Carla is running and the URL and Port are correct.") + "\n"
  343. fullError += self.tr("If it still does not work, check your current device and the remote's firewall.")
  344. CustomMessageBox(self,
  345. QMessageBox.Warning,
  346. self.tr("Error"),
  347. self.tr("Connection failed"),
  348. fullError,
  349. QMessageBox.Ok,
  350. QMessageBox.Ok)
  351. return
  352. self.host.lo_server_tcp = lo_server_tcp
  353. self.host.lo_target_tcp = lo_target_tcp
  354. self.host.lo_target_tcp_name = lo_target_tcp_name
  355. self.host.lo_server_udp = lo_server_udp
  356. self.host.lo_target_udp = lo_target_udp
  357. self.host.lo_target_udp_name = lo_target_udp_name
  358. self.ui.act_file_refresh.setEnabled(True)
  359. self.startTimers()
  360. def disconnectOsc(self):
  361. self.killTimers()
  362. self.unregister()
  363. self.removeAllPlugins()
  364. patchcanvas.clear()
  365. self.ui.act_file_refresh.setEnabled(False)
  366. # --------------------------------------------------------------------------------------------------------
  367. def unregister(self):
  368. if self.host.lo_server_tcp is not None:
  369. if self.host.lo_target_tcp is not None:
  370. try:
  371. lo_send(self.host.lo_target_tcp, "/unregister", self.host.lo_server_tcp.getFullURL())
  372. except:
  373. pass
  374. self.host.lo_target_tcp = None
  375. while self.host.lo_server_tcp.recv(0):
  376. pass
  377. #self.host.lo_server_tcp.free()
  378. self.host.lo_server_tcp = None
  379. if self.host.lo_server_udp is not None:
  380. if self.host.lo_target_udp is not None:
  381. try:
  382. lo_send(self.host.lo_target_udp, "/unregister", self.host.lo_server_udp.getFullURL())
  383. except:
  384. pass
  385. self.host.lo_target_udp = None
  386. while self.host.lo_server_udp.recv(0):
  387. pass
  388. #self.host.lo_server_udp.free()
  389. self.host.lo_server_udp = None
  390. self.host.lo_target_tcp_name = ""
  391. self.host.lo_target_udp_name = ""
  392. # --------------------------------------------------------------------------------------------------------
  393. # Timers
  394. def idleFast(self):
  395. HostWindow.idleFast(self)
  396. if self.host.lo_server_tcp is not None:
  397. self.host.lo_server_tcp.idle()
  398. else:
  399. self.disconnectOsc()
  400. if self.host.lo_server_udp is not None:
  401. self.host.lo_server_udp.idle()
  402. else:
  403. self.disconnectOsc()
  404. # --------------------------------------------------------------------------------------------------------
  405. def removeAllPlugins(self):
  406. self.host.fPluginsInfo = []
  407. HostWindow.removeAllPlugins(self)
  408. # --------------------------------------------------------------------------------------------------------
  409. def loadSettings(self, firstTime):
  410. settings = HostWindow.loadSettings(self, firstTime)
  411. self.fOscAddressTCP = settings.value("RemoteAddressTCP", "osc.tcp://127.0.0.1:22752/Carla", type=str)
  412. self.fOscAddressUDP = settings.value("RemoteAddressUDP", "osc.udp://127.0.0.1:22752/Carla", type=str)
  413. def saveSettings(self):
  414. settings = HostWindow.saveSettings(self)
  415. if self.fOscAddressTCP:
  416. settings.setValue("RemoteAddressTCP", self.fOscAddressTCP)
  417. if self.fOscAddressUDP:
  418. settings.setValue("RemoteAddressUDP", self.fOscAddressUDP)
  419. # --------------------------------------------------------------------------------------------------------
  420. @pyqtSlot()
  421. def slot_fileConnect(self):
  422. dialog = QInputDialog(self)
  423. dialog.setInputMode(QInputDialog.TextInput)
  424. dialog.setLabelText(self.tr("Address:"))
  425. dialog.setTextValue(self.fOscAddressTCP or "osc.tcp://127.0.0.1:22752/Carla")
  426. dialog.setWindowTitle(self.tr("Carla Control - Connect"))
  427. dialog.resize(400,1)
  428. ok = dialog.exec_()
  429. addr = dialog.textValue().strip()
  430. del dialog
  431. if not ok:
  432. return
  433. if not addr:
  434. return
  435. self.disconnectOsc()
  436. if addr:
  437. self.connectOsc(addr.replace("osc.udp:", "osc.tcp:"), addr.replace("osc.tcp:", "osc.udp:"))
  438. @pyqtSlot()
  439. def slot_fileRefresh(self):
  440. if None in (self.host.lo_server_tcp, self.host.lo_server_udp, self.host.lo_target_tcp, self.host.lo_target_udp):
  441. return
  442. lo_send(self.host.lo_target_udp, "/unregister", self.host.lo_server_udp.getFullURL())
  443. while self.host.lo_server_udp.recv(0):
  444. pass
  445. #self.host.lo_server_udp.free()
  446. lo_send(self.host.lo_target_tcp, "/unregister", self.host.lo_server_tcp.getFullURL())
  447. while self.host.lo_server_tcp.recv(0):
  448. pass
  449. #self.host.lo_server_tcp.free()
  450. self.removeAllPlugins()
  451. patchcanvas.clear()
  452. self.host.lo_server_tcp = CarlaControlServerTCP(self.host)
  453. self.host.lo_server_udp = CarlaControlServerUDP(self.host)
  454. try:
  455. lo_send(self.host.lo_target_tcp, "/register", self.host.lo_server_tcp.getFullURL())
  456. except:
  457. self.disconnectOsc()
  458. return
  459. try:
  460. lo_send(self.host.lo_target_udp, "/register", self.host.lo_server_udp.getFullURL())
  461. except:
  462. self.disconnectOsc()
  463. return
  464. @pyqtSlot()
  465. def slot_handleQuitCallback(self):
  466. self.disconnectOsc()
  467. HostWindow.slot_handleQuitCallback(self)
  468. # --------------------------------------------------------------------------------------------------------
  469. def closeEvent(self, event):
  470. self.killTimers()
  471. self.unregister()
  472. HostWindow.closeEvent(self, event)
  473. # ------------------------------------------------------------------------------------------------------------