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.

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