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.

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