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.

497 lines
17KB

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