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.

472 lines
16KB

  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 make_method, Address, ServerError, ServerThread
  23. from liblo import send as lo_send
  24. from liblo import TCP as LO_TCP
  25. from liblo import UDP as LO_UDP
  26. # ------------------------------------------------------------------------------------------------------------
  27. # Global liblo objects
  28. global lo_target, lo_target_name
  29. lo_target = None
  30. lo_target_name = ""
  31. # ------------------------------------------------------------------------------------------------------------
  32. # Host OSC object
  33. class CarlaHostOSC(CarlaHostQtPlugin):
  34. def __init__(self):
  35. CarlaHostQtPlugin.__init__(self)
  36. # -------------------------------------------------------------------
  37. def printAndReturnError(self, error):
  38. print(error)
  39. self.fLastError = error
  40. return False
  41. def sendMsg(self, lines):
  42. global lo_target, lo_target_name
  43. if lo_target is None:
  44. return self.printAndReturnError("lo_target is None")
  45. if lo_target_name is None:
  46. return self.printAndReturnError("lo_target_name is None")
  47. if len(lines) < 2:
  48. return self.printAndReturnError("not enough arguments")
  49. method = lines.pop(0)
  50. if method not in (
  51. #"set_option",
  52. "set_active",
  53. "set_drywet",
  54. "set_volume",
  55. "set_balance_left",
  56. "set_balance_right",
  57. "set_panning",
  58. #"set_ctrl_channel",
  59. "set_parameter_value",
  60. "set_parameter_midi_channel",
  61. "set_parameter_midi_cc",
  62. "set_program",
  63. "set_midi_program",
  64. #"set_custom_data",
  65. #"set_chunk_data",
  66. #"prepare_for_save",
  67. #"reset_parameters",
  68. #"randomize_parameters",
  69. "send_midi_note"
  70. ):
  71. return self.printAndReturnError("invalid method '%s'" % method)
  72. pluginId = lines.pop(0)
  73. args = []
  74. if method == "send_midi_note":
  75. channel, note, velocity = lines
  76. if velocity:
  77. method = "note_on"
  78. args = [channel, note, velocity]
  79. else:
  80. method = "note_off"
  81. args = [channel, note]
  82. else:
  83. for line in lines:
  84. if isinstance(line, bool):
  85. args.append(int(line))
  86. else:
  87. args.append(line)
  88. path = "/%s/%i/%s" % (lo_target_name, pluginId, method)
  89. print(path, args)
  90. lo_send(lo_target, path, *args)
  91. return True
  92. # -------------------------------------------------------------------
  93. def engine_init(self, driverName, clientName):
  94. global lo_target
  95. return lo_target is not None
  96. def engine_close(self):
  97. return True
  98. def engine_idle(self):
  99. return
  100. def is_engine_running(self):
  101. global lo_target
  102. return lo_target is not None
  103. def set_engine_about_to_close(self):
  104. return
  105. # ------------------------------------------------------------------------------------------------------------
  106. # OSC Control server
  107. class CarlaControlServerThread(ServerThread):
  108. def __init__(self, host, mode):
  109. ServerThread.__init__(self, 8087, mode)
  110. self.host = host
  111. def getFullURL(self):
  112. return "%scarla-control" % self.get_url()
  113. @make_method('/carla-control/add_plugin_start', 'is') # FIXME skip name
  114. def add_plugin_start_callback(self, path, args):
  115. pluginId, pluginName = args
  116. self.host._add(pluginId)
  117. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  118. @make_method('/carla-control/add_plugin_end', 'i') # FIXME skip name
  119. def add_plugin_end_callback(self, path, args):
  120. pluginId, = args
  121. self.host.PluginAddedCallback.emit(pluginId, "") #self.fPluginsInfo[pluginId].pluginInfo['name'])
  122. @make_method('/carla-control/remove_plugin', 'i')
  123. def remove_plugin_callback(self, path, args):
  124. pluginId, = args
  125. self.host.PluginRemovedCallback.emit(pluginId)
  126. @make_method('/carla-control/set_plugin_info1', 'iiiih')
  127. def set_plugin_info1_callback(self, path, args):
  128. pluginId, type_, category, hints, uniqueId = args # , optsAvail, optsEnabled
  129. optsAvail = optsEnabled = 0x0 # FIXME
  130. hints &= ~PLUGIN_HAS_CUSTOM_UI
  131. pinfo = {
  132. 'type': type_,
  133. 'category': category,
  134. 'hints': hints,
  135. 'optionsAvailable': optsAvail,
  136. 'optionsEnabled': optsEnabled,
  137. 'uniqueId': uniqueId
  138. }
  139. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  140. @make_method('/carla-control/set_plugin_info2', 'issss')
  141. def set_plugin_info2_callback(self, path, args):
  142. pluginId, realName, label, maker, copyright = args # , filename, name, iconName
  143. filename = name = iconName = "" # FIXME
  144. pinfo = {
  145. 'filename': filename,
  146. #'name': name, # FIXME
  147. 'label': label,
  148. 'maker': maker,
  149. 'copyright': copyright,
  150. 'iconName': iconName
  151. }
  152. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  153. self.host._set_pluginRealName(pluginId, realName)
  154. @make_method('/carla-control/set_audio_count', 'iii')
  155. def set_audio_count_callback(self, path, args):
  156. pluginId, ins, outs = args
  157. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  158. @make_method('/carla-control/set_midi_count', 'iii')
  159. def set_midi_count_callback(self, path, args):
  160. pluginId, ins, outs = args
  161. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  162. @make_method('/carla-control/set_parameter_count', 'iii') # FIXME
  163. def set_parameter_count_callback(self, path, args):
  164. pluginId, ins, outs = args # , count
  165. count = ins + outs
  166. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  167. @make_method('/carla-control/set_program_count', 'ii')
  168. def set_program_count_callback(self, path, args):
  169. pluginId, count = args
  170. self.host._set_programCount(pluginId, count)
  171. @make_method('/carla-control/set_midi_program_count', 'ii')
  172. def set_midi_program_count_callback(self, path, args):
  173. pluginId, count = args
  174. self.host._set_midiProgramCount(pluginId, count)
  175. @make_method('/carla-control/set_parameter_data', 'iiiiss')
  176. def set_parameter_data_callback(self, path, args):
  177. pluginId, paramId, type_, hints, name, unit = args
  178. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  179. paramInfo = {
  180. 'name': name,
  181. 'symbol': "",
  182. 'unit': unit,
  183. 'scalePointCount': 0,
  184. }
  185. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  186. paramData = {
  187. 'type': type_,
  188. 'hints': hints,
  189. 'index': paramId,
  190. 'rindex': -1,
  191. 'midiCC': -1,
  192. 'midiChannel': 0
  193. }
  194. self.host._set_parameterData(pluginId, paramId, paramData)
  195. @make_method('/carla-control/set_parameter_ranges1', 'iifff')
  196. def set_parameter_ranges1_callback(self, path, args):
  197. pluginId, paramId, def_, min_, max_ = args
  198. paramRanges = {
  199. 'def': def_,
  200. 'min': min_,
  201. 'max': max_
  202. }
  203. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  204. @make_method('/carla-control/set_parameter_ranges2', 'iifff')
  205. def set_parameter_ranges2_callback(self, path, args):
  206. pluginId, paramId, step, stepSmall, stepLarge = args
  207. paramRanges = {
  208. 'step': step,
  209. 'stepSmall': stepSmall,
  210. 'stepLarge': stepLarge
  211. }
  212. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  213. @make_method('/carla-control/set_parameter_midi_cc', 'iii')
  214. def set_parameter_midi_cc_callback(self, path, args):
  215. pluginId, paramId, cc = args
  216. self.host._set_parameterMidiCC(pluginId, paramId, cc)
  217. self.host.ParameterMidiCcChangedCallback.emit(pluginId, paramId, cc)
  218. @make_method('/carla-control/set_parameter_midi_channel', 'iii')
  219. def set_parameter_midi_channel_callback(self, path, args):
  220. pluginId, paramId, channel = args
  221. self.host._set_parameterMidiChannel(pluginId, paramId, channel)
  222. self.host.ParameterMidiChannelChangedCallback.emit(pluginId, paramId, channel)
  223. @make_method('/carla-control/set_parameter_value', 'iif')
  224. def set_parameter_value_callback(self, path, args):
  225. pluginId, paramId, paramValue = args
  226. if paramId < 0:
  227. self.host._set_internalValue(pluginId, paramId, paramValue)
  228. else:
  229. self.host._set_parameterValue(pluginId, paramId, paramValue)
  230. self.host.ParameterValueChangedCallback.emit(pluginId, paramId, paramValue)
  231. @make_method('/carla-control/set_default_value', 'iif')
  232. def set_default_value_callback(self, path, args):
  233. pluginId, paramId, paramValue = args
  234. self.host._set_parameterDefault(pluginId, paramId, paramValue)
  235. self.host.ParameterDefaultChangedCallback.emit(pluginId, paramId, paramValue)
  236. @make_method('/carla-control/set_current_program', 'ii')
  237. def set_current_program_callback(self, path, args):
  238. pluginId, current = args
  239. self.host._set_currentProgram(pluginId, current)
  240. self.host.ProgramChangedCallback.emit(current)
  241. @make_method('/carla-control/set_current_midi_program', 'ii')
  242. def set_current_midi_program_callback(self, path, args):
  243. pluginId, current = args
  244. self.host._set_currentMidiProgram(pluginId, current)
  245. #self.host.MidiProgramChangedCallback.emit() # FIXME
  246. @make_method('/carla-control/set_program_name', 'iis')
  247. def set_program_name_callback(self, path, args):
  248. pluginId, progId, progName = args
  249. self.host._set_programName(pluginId, progId, progName)
  250. @make_method('/carla-control/set_midi_program_data', 'iiiis')
  251. def set_midi_program_data_callback(self, path, args):
  252. pluginId, midiProgId, bank, program, name = args
  253. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  254. @make_method('/carla-control/note_on', 'iiii')
  255. def set_note_on_callback(self, path, args):
  256. pluginId, channel, note, velocity = args
  257. self.host.NoteOnCallback.emit(pluginId, channel, note, velocity)
  258. @make_method('/carla-control/note_off', 'iii')
  259. def set_note_off_callback(self, path, args):
  260. pluginId, channel, note = args
  261. self.host.NoteOffCallback.emit(pluginId, channel, note)
  262. @make_method('/carla-control/set_peaks', 'iffff')
  263. def set_peaks_callback(self, path, args):
  264. pluginId, in1, in2, out1, out2 = args
  265. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  266. @make_method('/carla-control/exit', '')
  267. def set_exit_callback(self, path, args):
  268. self.host.QuitCallback.emit()
  269. @make_method(None, None)
  270. def fallback(self, path, args):
  271. print("ControlServer::fallback(\"%s\") - unknown message, args =" % path, args)
  272. # ------------------------------------------------------------------------------------------------------------
  273. # Main Window
  274. class HostWindowOSC(HostWindow):
  275. def __init__(self, host, oscAddr):
  276. HostWindow.__init__(self, host, False)
  277. self.host = host
  278. if False:
  279. # kdevelop likes this :)
  280. host = CarlaHostPlugin()
  281. self.host = host
  282. # ----------------------------------------------------------------------------------------------------
  283. # Internal stuff
  284. self.fOscAddress = oscAddr
  285. self.fOscServer = None
  286. # ----------------------------------------------------------------------------------------------------
  287. # Set up GUI (not connected)
  288. self.ui.act_file_refresh.setEnabled(False)
  289. # ----------------------------------------------------------------------------------------------------
  290. # Connect actions to functions
  291. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  292. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  293. # ----------------------------------------------------------------------------------------------------
  294. # Final setup
  295. if oscAddr:
  296. QTimer.singleShot(0, self.connectOsc)
  297. def connectOsc(self, addr = None):
  298. global lo_target, lo_target_name
  299. if addr is not None:
  300. self.fOscAddress = addr
  301. lo_target = Address(self.fOscAddress)
  302. lo_target_name = self.fOscAddress.rsplit("/", 1)[-1]
  303. print("Connecting to \"%s\" as '%s'..." % (self.fOscAddress, lo_target_name))
  304. try:
  305. self.fOscServer = CarlaControlServerThread(self.host, LO_UDP if self.fOscAddress.startswith("osc.udp") else LO_TCP)
  306. except: # ServerError as err:
  307. QMessageBox.critical(self, self.tr("Error"), self.tr("Failed to connect, operation failed."))
  308. return
  309. self.fOscServer.start()
  310. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  311. self.startTimers()
  312. self.ui.act_file_refresh.setEnabled(True)
  313. def disconnectOsc(self):
  314. global lo_target, lo_target_name
  315. self.killTimers()
  316. self.ui.act_file_refresh.setEnabled(False)
  317. if lo_target is not None:
  318. lo_send(lo_target, "/unregister")
  319. if self.fOscServer is not None:
  320. self.fOscServer.stop()
  321. self.fOscServer = None
  322. self.removeAllPlugins()
  323. lo_target = None
  324. lo_target_name = ""
  325. self.fOscAddress = ""
  326. def removeAllPlugins(self):
  327. self.host.fPluginsInfo = []
  328. HostWindow.removeAllPlugins(self)
  329. @pyqtSlot()
  330. def slot_fileConnect(self):
  331. global lo_target, lo_target_name
  332. if lo_target and self.fOscServer:
  333. urlText = self.fOscAddress
  334. else:
  335. urlText = "osc.tcp://127.0.0.1:19000/Carla"
  336. addr, ok = QInputDialog.getText(self, self.tr("Carla Control - Connect"), self.tr("Address"), text=urlText)
  337. if not ok:
  338. return
  339. self.disconnectOsc()
  340. self.connectOsc(addr)
  341. @pyqtSlot()
  342. def slot_fileRefresh(self):
  343. global lo_target
  344. if lo_target is None or self.fOscServer is None:
  345. return
  346. self.killTimers()
  347. lo_send(lo_target, "/unregister")
  348. self.removeAllPlugins()
  349. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  350. self.startTimers()
  351. @pyqtSlot()
  352. def slot_handleQuitCallback(self):
  353. self.disconnectOsc()
  354. def closeEvent(self, event):
  355. global lo_target
  356. self.killTimers()
  357. if lo_target is not None and self.fOscServer is not None:
  358. lo_send(lo_target, "/unregister")
  359. HostWindow.closeEvent(self, event)
  360. # ------------------------------------------------------------------------------------------------------------