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.

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