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.

534 lines
18KB

  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_target = None
  39. self.lo_target_name = ""
  40. # -------------------------------------------------------------------
  41. def printAndReturnError(self, error):
  42. print(error)
  43. self.fLastError = error
  44. return False
  45. def sendMsg(self, lines):
  46. if len(lines) < 1:
  47. return self.printAndReturnError("not enough arguments")
  48. method = lines.pop(0)
  49. if method == "set_engine_option":
  50. return True
  51. if self.lo_target is None:
  52. return self.printAndReturnError("lo_target is None")
  53. if self.lo_target_name is None:
  54. return self.printAndReturnError("lo_target_name is None")
  55. if method not in (
  56. #"set_option",
  57. "set_active",
  58. "set_drywet",
  59. "set_volume",
  60. "set_balance_left",
  61. "set_balance_right",
  62. "set_panning",
  63. #"set_ctrl_channel",
  64. "set_parameter_value",
  65. "set_parameter_midi_channel",
  66. "set_parameter_midi_cc",
  67. "set_program",
  68. "set_midi_program",
  69. #"set_custom_data",
  70. #"set_chunk_data",
  71. #"prepare_for_save",
  72. #"reset_parameters",
  73. #"randomize_parameters",
  74. "send_midi_note"
  75. ):
  76. return self.printAndReturnError("invalid method '%s'" % method)
  77. pluginId = lines.pop(0)
  78. args = []
  79. if method == "send_midi_note":
  80. channel, note, velocity = lines
  81. if velocity:
  82. method = "note_on"
  83. args = [channel, note, velocity]
  84. else:
  85. method = "note_off"
  86. args = [channel, note]
  87. else:
  88. for line in lines:
  89. if isinstance(line, bool):
  90. args.append(int(line))
  91. else:
  92. args.append(line)
  93. path = "/%s/%i/%s" % (self.lo_target_name, pluginId, method)
  94. print(path, args)
  95. lo_send(self.lo_target, path, *args)
  96. return True
  97. # -------------------------------------------------------------------
  98. def engine_init(self, driverName, clientName):
  99. return self.lo_target is not None
  100. def engine_close(self):
  101. return True
  102. def engine_idle(self):
  103. return
  104. def is_engine_running(self):
  105. return self.lo_target is not None
  106. def set_engine_about_to_close(self):
  107. return
  108. # ------------------------------------------------------------------------------------------------------------
  109. # OSC Control server
  110. class CarlaControlServer(Server):
  111. def __init__(self, host, mode):
  112. Server.__init__(self, 8998 + int(random()*9000), mode)
  113. self.host = host
  114. def idle(self):
  115. self.fReceivedMsgs = False
  116. while self.recv(0) and self.fReceivedMsgs:
  117. pass
  118. def getFullURL(self):
  119. return "%scarla-control" % self.get_url()
  120. @make_method('/carla-control/cb', 'iiiiifs')
  121. def carla_cb(self, path, args):
  122. print(path, args)
  123. self.fReceivedMsgs = True
  124. action, pluginId, value1, value2, value3, valuef, valueStr = args
  125. self.host._setViaCallback(action, pluginId, value1, value2, value3, valuef, valueStr)
  126. engineCallback(self.host, action, pluginId, value1, value2, value3, valuef, valueStr)
  127. @make_method('/carla-control/init', 'is') # FIXME set name in add method
  128. def carla_init(self, path, args):
  129. print(path, args)
  130. self.fReceivedMsgs = True
  131. pluginId, pluginName = args
  132. self.host._add(pluginId)
  133. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  134. @make_method('/carla-control/ports', 'iiiiiiii')
  135. def carla_ports(self, path, args):
  136. print(path, args)
  137. self.fReceivedMsgs = True
  138. pluginId, audioIns, audioOuts, midiIns, midiOuts, paramIns, paramOuts, paramTotal = args
  139. self.host._set_audioCountInfo(pluginId, {'ins': audioIns, 'outs': audioOuts})
  140. self.host._set_midiCountInfo(pluginId, {'ins': midiOuts, 'outs': midiOuts})
  141. self.host._set_parameterCountInfo(pluginId, paramTotal, {'ins': paramIns, 'outs': paramOuts})
  142. @make_method('/carla-control/info', 'iiiihssss')
  143. def carla_info(self, path, args):
  144. print(path, args)
  145. self.fReceivedMsgs = True
  146. pluginId, type_, category, hints, uniqueId, realName, label, maker, copyright = args
  147. optsAvail = optsEnabled = 0x0 # FIXME
  148. filename = name = iconName = "" # FIXME
  149. hints &= ~PLUGIN_HAS_CUSTOM_UI
  150. pinfo = {
  151. 'type': type_,
  152. 'category': category,
  153. 'hints': hints,
  154. 'optionsAvailable': optsAvail,
  155. 'optionsEnabled': optsEnabled,
  156. 'uniqueId': uniqueId,
  157. 'filename': filename,
  158. #'name': name, # FIXME
  159. 'label': label,
  160. 'maker': maker,
  161. 'copyright': copyright,
  162. 'iconName': iconName
  163. }
  164. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  165. self.host._set_pluginRealName(pluginId, realName)
  166. @make_method('/carla-control/param', 'iiiiiissfffffff')
  167. def carla_param(self, path, args):
  168. print(path, args)
  169. self.fReceivedMsgs = True
  170. (
  171. pluginId, paramId, type_, hints, midiChan, midiCC, name, unit,
  172. def_, min_, max_, step, stepSmall, stepLarge, value
  173. ) = args
  174. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  175. paramInfo = {
  176. 'name': name,
  177. 'symbol': "",
  178. 'unit': unit,
  179. 'scalePointCount': 0,
  180. }
  181. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  182. paramData = {
  183. 'type': type_,
  184. 'hints': hints,
  185. 'index': paramId,
  186. 'rindex': -1,
  187. 'midiCC': midiCC,
  188. 'midiChannel': midiChan,
  189. }
  190. self.host._set_parameterData(pluginId, paramId, paramData)
  191. paramRanges = {
  192. 'def': def_,
  193. 'min': min_,
  194. 'max': max_,
  195. 'step': step,
  196. 'stepSmall': stepSmall,
  197. 'stepLarge': stepLarge,
  198. }
  199. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  200. self.host._set_parameterValue(pluginId, paramId, value)
  201. @make_method('/carla-control/iparams', 'ifffffff')
  202. def carla_iparams(self, path, args):
  203. print(path, args)
  204. self.fReceivedMsgs = True
  205. pluginId, active, drywet, volume, balLeft, balRight, pan, ctrlChan = args
  206. self.host._set_internalValue(pluginId, PARAMETER_ACTIVE, active)
  207. self.host._set_internalValue(pluginId, PARAMETER_DRYWET, drywet)
  208. self.host._set_internalValue(pluginId, PARAMETER_VOLUME, volume)
  209. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_LEFT, balLeft)
  210. self.host._set_internalValue(pluginId, PARAMETER_BALANCE_RIGHT, balRight)
  211. self.host._set_internalValue(pluginId, PARAMETER_PANNING, pan)
  212. self.host._set_internalValue(pluginId, PARAMETER_CTRL_CHANNEL, ctrlChan)
  213. @make_method('/carla-control/set_program_count', 'ii')
  214. def set_program_count_callback(self, path, args):
  215. print(path, args)
  216. self.fReceivedMsgs = True
  217. pluginId, count = args
  218. self.host._set_programCount(pluginId, count)
  219. @make_method('/carla-control/set_midi_program_count', 'ii')
  220. def set_midi_program_count_callback(self, path, args):
  221. print(path, args)
  222. self.fReceivedMsgs = True
  223. pluginId, count = args
  224. self.host._set_midiProgramCount(pluginId, count)
  225. @make_method('/carla-control/set_program_name', 'iis')
  226. def set_program_name_callback(self, path, args):
  227. print(path, args)
  228. self.fReceivedMsgs = True
  229. pluginId, progId, progName = args
  230. self.host._set_programName(pluginId, progId, progName)
  231. @make_method('/carla-control/set_midi_program_data', 'iiiis')
  232. def set_midi_program_data_callback(self, path, args):
  233. print(path, args)
  234. self.fReceivedMsgs = True
  235. pluginId, midiProgId, bank, program, name = args
  236. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  237. #@make_method('/carla-control/note_on', 'iiii')
  238. @make_method('/carla-control/runtime', 'fiihiiif')
  239. def carla_runtime(self, path, args):
  240. self.fReceivedMsgs = True
  241. load, xruns, playing, frame, bar, beat, tick, bpm = args
  242. self.host._set_runtime_info(load, xruns)
  243. self.host._set_transport(bool(playing), frame, bar, beat, tick, bpm)
  244. @make_method('/carla-control/param_fixme', 'iif')
  245. def carla_param_fixme(self, path, args):
  246. self.fReceivedMsgs = True
  247. pluginId, paramId, paramValue = args
  248. self.host._set_parameterValue(pluginId, paramId, paramValue)
  249. @make_method('/carla-control/peaks', 'iffff')
  250. def carla_peaks(self, path, args):
  251. self.fReceivedMsgs = True
  252. pluginId, in1, in2, out1, out2 = args
  253. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  254. #@make_method('/carla-control/note_on', 'iiii')
  255. @make_method('/carla-control/exit-error', 's')
  256. def set_exit_error_callback(self, path, args):
  257. print(path, args)
  258. self.fReceivedMsgs = True
  259. error, = args
  260. self.host.lo_target = None
  261. self.host.QuitCallback.emit()
  262. self.host.ErrorCallback.emit(error)
  263. @make_method(None, None)
  264. def fallback(self, path, args):
  265. print("ControlServer::fallback(\"%s\") - unknown message, args =" % path, args)
  266. self.fReceivedMsgs = True
  267. # ------------------------------------------------------------------------------------------------------------
  268. # Main Window
  269. class HostWindowOSC(HostWindow):
  270. def __init__(self, host, oscAddr):
  271. HostWindow.__init__(self, host, True)
  272. self.host = host
  273. if False:
  274. # kdevelop likes this :)
  275. host = CarlaHostPlugin()
  276. self.host = host
  277. # ----------------------------------------------------------------------------------------------------
  278. # Internal stuff
  279. self.fOscServer = None
  280. # ----------------------------------------------------------------------------------------------------
  281. # Connect actions to functions
  282. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  283. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  284. # ----------------------------------------------------------------------------------------------------
  285. # Final setup
  286. if oscAddr:
  287. QTimer.singleShot(0, self.connectOsc)
  288. def connectOsc(self, addr = None):
  289. if addr is not None:
  290. self.fOscAddress = addr
  291. lo_protocol = LO_UDP if self.fOscAddress.startswith("osc.udp") else LO_TCP
  292. lo_target_name = self.fOscAddress.rsplit("/", 1)[-1]
  293. err = None
  294. print("Connecting to \"%s\" as '%s'..." % (self.fOscAddress, lo_target_name))
  295. try:
  296. lo_target = Address(self.fOscAddress)
  297. self.fOscServer = CarlaControlServer(self.host, lo_protocol)
  298. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  299. except AddressError as e:
  300. err = e
  301. except OSError as e:
  302. err = e
  303. except:
  304. err = { 'args': [] }
  305. if err is not None:
  306. del self.fOscServer
  307. self.fOscServer = None
  308. fullError = self.tr("Failed to connect to the Carla instance.")
  309. if len(err.args) > 0:
  310. fullError += " %s\n%s\n" % (self.tr("Error was:"), err.args[0])
  311. fullError += "\n"
  312. fullError += self.tr("Make sure the remote Carla is running and the URL and Port are correct.") + "\n"
  313. fullError += self.tr("If it still does not work, check your current device and the remote's firewall.")
  314. CustomMessageBox(self,
  315. QMessageBox.Warning,
  316. self.tr("Error"),
  317. self.tr("Connection failed"),
  318. fullError,
  319. QMessageBox.Ok,
  320. QMessageBox.Ok)
  321. return
  322. self.host.lo_target = lo_target
  323. self.host.lo_target_name = lo_target_name
  324. self.ui.act_file_refresh.setEnabled(True)
  325. self.startTimers()
  326. def disconnectOsc(self):
  327. self.killTimers()
  328. if self.host.lo_target is not None:
  329. try:
  330. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  331. except:
  332. pass
  333. if self.fOscServer is not None:
  334. del self.fOscServer
  335. self.fOscServer = None
  336. self.removeAllPlugins()
  337. self.host.lo_target = None
  338. self.host.lo_target_name = ""
  339. self.ui.act_file_refresh.setEnabled(False)
  340. # --------------------------------------------------------------------------------------------------------
  341. # Timers
  342. def startTimers(self):
  343. if self.fIdleTimerOSC == 0:
  344. self.fIdleTimerOSC = self.startTimer(20)
  345. HostWindow.startTimers(self)
  346. def restartTimersIfNeeded(self):
  347. if self.fIdleTimerOSC != 0:
  348. self.killTimer(self.fIdleTimerOSC)
  349. self.fIdleTimerOSC = self.startTimer(20)
  350. HostWindow.restartTimersIfNeeded(self)
  351. def killTimers(self):
  352. if self.fIdleTimerOSC != 0:
  353. self.killTimer(self.fIdleTimerOSC)
  354. self.fIdleTimerOSC = 0
  355. HostWindow.killTimers(self)
  356. # --------------------------------------------------------------------------------------------------------
  357. def removeAllPlugins(self):
  358. self.host.fPluginsInfo = []
  359. HostWindow.removeAllPlugins(self)
  360. # --------------------------------------------------------------------------------------------------------
  361. def loadSettings(self, firstTime):
  362. settings = HostWindow.loadSettings(self, firstTime)
  363. self.fOscAddress = settings.value("RemoteAddress", "osc.tcp://127.0.0.1:22752/Carla", type=str)
  364. def saveSettings(self):
  365. settings = HostWindow.saveSettings(self)
  366. if self.fOscAddress:
  367. settings.setValue("RemoteAddress", self.fOscAddress)
  368. # --------------------------------------------------------------------------------------------------------
  369. @pyqtSlot()
  370. def slot_fileConnect(self):
  371. dialog = QInputDialog(self)
  372. dialog.setInputMode(QInputDialog.TextInput)
  373. dialog.setLabelText(self.tr("Address:"))
  374. dialog.setTextValue(self.fOscAddress or "osc.tcp://127.0.0.1:22752/Carla")
  375. dialog.setWindowTitle(self.tr("Carla Control - Connect"))
  376. dialog.resize(400,1)
  377. ok = dialog.exec_()
  378. addr = dialog.textValue().strip()
  379. del dialog
  380. if not ok:
  381. return
  382. if not addr:
  383. return
  384. self.disconnectOsc()
  385. if addr:
  386. self.connectOsc(addr)
  387. @pyqtSlot()
  388. def slot_fileRefresh(self):
  389. if self.host.lo_target is None or self.fOscServer is None:
  390. return
  391. self.killTimers()
  392. self.removeAllPlugins()
  393. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  394. lo_send(self.host.lo_target, "/register", self.fOscServer.getFullURL())
  395. self.startTimers()
  396. @pyqtSlot()
  397. def slot_handleQuitCallback(self):
  398. HostWindow.slot_handleQuitCallback(self)
  399. self.disconnectOsc()
  400. # --------------------------------------------------------------------------------------------------------
  401. def timerEvent(self, event):
  402. if event.timerId() == self.fIdleTimerOSC:
  403. self.fOscServer.idle()
  404. if self.host.lo_target is None:
  405. self.disconnectOsc()
  406. HostWindow.timerEvent(self, event)
  407. def closeEvent(self, event):
  408. self.killTimers()
  409. if self.host.lo_target is not None and self.fOscServer is not None:
  410. try:
  411. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  412. except:
  413. pass
  414. HostWindow.closeEvent(self, event)
  415. # ------------------------------------------------------------------------------------------------------------