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.

615 lines
21KB

  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/add_plugin_start', 'is') # FIXME skip name
  121. def add_plugin_start_callback(self, path, args):
  122. print(path, args)
  123. self.fReceivedMsgs = True
  124. pluginId, pluginName = args
  125. self.host._add(pluginId)
  126. self.host._set_pluginInfoUpdate(pluginId, {'name': pluginName})
  127. @make_method('/carla-control/add_plugin_end', 'i') # FIXME skip name
  128. def add_plugin_end_callback(self, path, args):
  129. print(path, args)
  130. self.fReceivedMsgs = True
  131. pluginId, = args
  132. self.host.PluginAddedCallback.emit(pluginId, "") #self.fPluginsInfo[pluginId].pluginInfo['name'])
  133. @make_method('/carla-control/remove_plugin', 'i')
  134. def remove_plugin_callback(self, path, args):
  135. print(path, args)
  136. self.fReceivedMsgs = True
  137. pluginId, = args
  138. self.host.PluginRemovedCallback.emit(pluginId)
  139. @make_method('/carla-control/set_plugin_info1', 'iiiih')
  140. def set_plugin_info1_callback(self, path, args):
  141. print(path, args)
  142. self.fReceivedMsgs = True
  143. pluginId, type_, category, hints, uniqueId = args # , optsAvail, optsEnabled
  144. optsAvail = optsEnabled = 0x0 # FIXME
  145. hints &= ~PLUGIN_HAS_CUSTOM_UI
  146. pinfo = {
  147. 'type': type_,
  148. 'category': category,
  149. 'hints': hints,
  150. 'optionsAvailable': optsAvail,
  151. 'optionsEnabled': optsEnabled,
  152. 'uniqueId': uniqueId
  153. }
  154. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  155. @make_method('/carla-control/set_plugin_info2', 'issss')
  156. def set_plugin_info2_callback(self, path, args):
  157. print(path, args)
  158. self.fReceivedMsgs = True
  159. pluginId, realName, label, maker, copyright = args # , filename, name, iconName
  160. filename = name = iconName = "" # FIXME
  161. pinfo = {
  162. 'filename': filename,
  163. #'name': name, # FIXME
  164. 'label': label,
  165. 'maker': maker,
  166. 'copyright': copyright,
  167. 'iconName': iconName
  168. }
  169. self.host._set_pluginInfoUpdate(pluginId, pinfo)
  170. self.host._set_pluginRealName(pluginId, realName)
  171. @make_method('/carla-control/set_audio_count', 'iii')
  172. def set_audio_count_callback(self, path, args):
  173. print(path, args)
  174. self.fReceivedMsgs = True
  175. pluginId, ins, outs = args
  176. self.host._set_audioCountInfo(pluginId, {'ins': ins, 'outs': outs})
  177. @make_method('/carla-control/set_midi_count', 'iii')
  178. def set_midi_count_callback(self, path, args):
  179. print(path, args)
  180. self.fReceivedMsgs = True
  181. pluginId, ins, outs = args
  182. self.host._set_midiCountInfo(pluginId, {'ins': ins, 'outs': outs})
  183. @make_method('/carla-control/set_parameter_count', 'iii') # FIXME
  184. def set_parameter_count_callback(self, path, args):
  185. print(path, args)
  186. self.fReceivedMsgs = True
  187. pluginId, ins, outs = args # , count
  188. count = ins + outs
  189. self.host._set_parameterCountInfo(pluginId, count, {'ins': ins, 'outs': outs})
  190. @make_method('/carla-control/set_program_count', 'ii')
  191. def set_program_count_callback(self, path, args):
  192. print(path, args)
  193. self.fReceivedMsgs = True
  194. pluginId, count = args
  195. self.host._set_programCount(pluginId, count)
  196. @make_method('/carla-control/set_midi_program_count', 'ii')
  197. def set_midi_program_count_callback(self, path, args):
  198. print(path, args)
  199. self.fReceivedMsgs = True
  200. pluginId, count = args
  201. self.host._set_midiProgramCount(pluginId, count)
  202. @make_method('/carla-control/set_parameter_data', 'iiiiss')
  203. def set_parameter_data_callback(self, path, args):
  204. print(path, args)
  205. self.fReceivedMsgs = True
  206. pluginId, paramId, type_, hints, name, unit = args
  207. hints &= ~(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT)
  208. paramInfo = {
  209. 'name': name,
  210. 'symbol': "",
  211. 'unit': unit,
  212. 'scalePointCount': 0,
  213. }
  214. self.host._set_parameterInfo(pluginId, paramId, paramInfo)
  215. paramData = {
  216. 'type': type_,
  217. 'hints': hints,
  218. 'index': paramId,
  219. 'rindex': -1,
  220. 'midiCC': -1,
  221. 'midiChannel': 0
  222. }
  223. self.host._set_parameterData(pluginId, paramId, paramData)
  224. @make_method('/carla-control/set_parameter_ranges1', 'iifff')
  225. def set_parameter_ranges1_callback(self, path, args):
  226. print(path, args)
  227. self.fReceivedMsgs = True
  228. pluginId, paramId, def_, min_, max_ = args
  229. paramRanges = {
  230. 'def': def_,
  231. 'min': min_,
  232. 'max': max_
  233. }
  234. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  235. @make_method('/carla-control/set_parameter_ranges2', 'iifff')
  236. def set_parameter_ranges2_callback(self, path, args):
  237. print(path, args)
  238. self.fReceivedMsgs = True
  239. pluginId, paramId, step, stepSmall, stepLarge = args
  240. paramRanges = {
  241. 'step': step,
  242. 'stepSmall': stepSmall,
  243. 'stepLarge': stepLarge
  244. }
  245. self.host._set_parameterRangesUpdate(pluginId, paramId, paramRanges)
  246. @make_method('/carla-control/set_parameter_midi_cc', 'iii')
  247. def set_parameter_midi_cc_callback(self, path, args):
  248. print(path, args)
  249. self.fReceivedMsgs = True
  250. pluginId, paramId, cc = args
  251. self.host._set_parameterMidiCC(pluginId, paramId, cc)
  252. self.host.ParameterMidiCcChangedCallback.emit(pluginId, paramId, cc)
  253. @make_method('/carla-control/set_parameter_midi_channel', 'iii')
  254. def set_parameter_midi_channel_callback(self, path, args):
  255. print(path, args)
  256. self.fReceivedMsgs = True
  257. pluginId, paramId, channel = args
  258. self.host._set_parameterMidiChannel(pluginId, paramId, channel)
  259. self.host.ParameterMidiChannelChangedCallback.emit(pluginId, paramId, channel)
  260. @make_method('/carla-control/set_parameter_value', 'iif')
  261. def set_parameter_value_callback(self, path, args):
  262. pluginId, paramId, paramValue = args
  263. if paramId < 0:
  264. self.host._set_internalValue(pluginId, paramId, paramValue)
  265. else:
  266. self.host._set_parameterValue(pluginId, paramId, paramValue)
  267. self.host.ParameterValueChangedCallback.emit(pluginId, paramId, paramValue)
  268. @make_method('/carla-control/set_default_value', 'iif')
  269. def set_default_value_callback(self, path, args):
  270. print(path, args)
  271. self.fReceivedMsgs = True
  272. pluginId, paramId, paramValue = args
  273. self.host._set_parameterDefault(pluginId, paramId, paramValue)
  274. self.host.ParameterDefaultChangedCallback.emit(pluginId, paramId, paramValue)
  275. @make_method('/carla-control/set_current_program', 'ii')
  276. def set_current_program_callback(self, path, args):
  277. print(path, args)
  278. self.fReceivedMsgs = True
  279. pluginId, current = args
  280. self.host._set_currentProgram(pluginId, current)
  281. self.host.ProgramChangedCallback.emit(pluginId, current)
  282. @make_method('/carla-control/set_current_midi_program', 'ii')
  283. def set_current_midi_program_callback(self, path, args):
  284. print(path, args)
  285. self.fReceivedMsgs = True
  286. pluginId, current = args
  287. self.host._set_currentMidiProgram(pluginId, current)
  288. self.host.MidiProgramChangedCallback.emit(pluginId, current)
  289. @make_method('/carla-control/set_program_name', 'iis')
  290. def set_program_name_callback(self, path, args):
  291. print(path, args)
  292. self.fReceivedMsgs = True
  293. pluginId, progId, progName = args
  294. self.host._set_programName(pluginId, progId, progName)
  295. @make_method('/carla-control/set_midi_program_data', 'iiiis')
  296. def set_midi_program_data_callback(self, path, args):
  297. print(path, args)
  298. self.fReceivedMsgs = True
  299. pluginId, midiProgId, bank, program, name = args
  300. self.host._set_midiProgramData(pluginId, midiProgId, {'bank': bank, 'program': program, 'name': name})
  301. @make_method('/carla-control/note_on', 'iiii')
  302. def set_note_on_callback(self, path, args):
  303. print(path, args)
  304. self.fReceivedMsgs = True
  305. pluginId, channel, note, velocity = args
  306. self.host.NoteOnCallback.emit(pluginId, channel, note, velocity)
  307. @make_method('/carla-control/note_off', 'iii')
  308. def set_note_off_callback(self, path, args):
  309. print(path, args)
  310. self.fReceivedMsgs = True
  311. pluginId, channel, note = args
  312. self.host.NoteOffCallback.emit(pluginId, channel, note)
  313. @make_method('/carla-control/set_peaks', 'iffff')
  314. def set_peaks_callback(self, path, args):
  315. self.fReceivedMsgs = True
  316. pluginId, in1, in2, out1, out2 = args
  317. self.host._set_peaks(pluginId, in1, in2, out1, out2)
  318. @make_method('/carla-control/exit', '')
  319. def set_exit_callback(self, path, args):
  320. print(path, args)
  321. self.fReceivedMsgs = True
  322. self.host.QuitCallback.emit()
  323. @make_method('/carla-control/exit-error', 's')
  324. def set_exit_error_callback(self, path, args):
  325. print(path, args)
  326. self.fReceivedMsgs = True
  327. error, = args
  328. self.host.lo_target = None
  329. self.host.QuitCallback.emit()
  330. self.host.ErrorCallback.emit(error)
  331. @make_method(None, None)
  332. def fallback(self, path, args):
  333. print("ControlServer::fallback(\"%s\") - unknown message, args =" % path, args)
  334. self.fReceivedMsgs = True
  335. # ------------------------------------------------------------------------------------------------------------
  336. # Main Window
  337. class HostWindowOSC(HostWindow):
  338. def __init__(self, host, oscAddr):
  339. HostWindow.__init__(self, host, False)
  340. self.host = host
  341. if False:
  342. # kdevelop likes this :)
  343. host = CarlaHostPlugin()
  344. self.host = host
  345. # ----------------------------------------------------------------------------------------------------
  346. # Internal stuff
  347. self.fOscServer = None
  348. # ----------------------------------------------------------------------------------------------------
  349. # Connect actions to functions
  350. self.ui.act_file_connect.triggered.connect(self.slot_fileConnect)
  351. self.ui.act_file_refresh.triggered.connect(self.slot_fileRefresh)
  352. # ----------------------------------------------------------------------------------------------------
  353. # Final setup
  354. if oscAddr:
  355. QTimer.singleShot(0, self.connectOsc)
  356. def connectOsc(self, addr = None):
  357. if addr is not None:
  358. self.fOscAddress = addr
  359. lo_protocol = LO_UDP if self.fOscAddress.startswith("osc.udp") else LO_TCP
  360. lo_target_name = self.fOscAddress.rsplit("/", 1)[-1]
  361. err = None
  362. print("Connecting to \"%s\" as '%s'..." % (self.fOscAddress, lo_target_name))
  363. try:
  364. lo_target = Address(self.fOscAddress)
  365. self.fOscServer = CarlaControlServer(self.host, lo_protocol)
  366. lo_send(lo_target, "/register", self.fOscServer.getFullURL())
  367. except AddressError as e:
  368. err = e
  369. except OSError as e:
  370. err = e
  371. except:
  372. err = { 'args': [] }
  373. if err is not None:
  374. del self.fOscServer
  375. self.fOscServer = None
  376. fullError = self.tr("Failed to connect to the Carla instance.")
  377. if len(err.args) > 0:
  378. fullError += " %s\n%s\n" % (self.tr("Error was:"), err.args[0])
  379. fullError += "\n"
  380. fullError += self.tr("Make sure the remote Carla is running and the URL and Port are correct.") + "\n"
  381. fullError += self.tr("If it still does not work, check your current device and the remote's firewall.")
  382. CustomMessageBox(self,
  383. QMessageBox.Warning,
  384. self.tr("Error"),
  385. self.tr("Connection failed"),
  386. fullError,
  387. QMessageBox.Ok,
  388. QMessageBox.Ok)
  389. return
  390. self.host.lo_target = lo_target
  391. self.host.lo_target_name = lo_target_name
  392. self.ui.act_file_refresh.setEnabled(True)
  393. self.startTimers()
  394. def disconnectOsc(self):
  395. self.killTimers()
  396. if self.host.lo_target is not None:
  397. try:
  398. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  399. except:
  400. pass
  401. if self.fOscServer is not None:
  402. del self.fOscServer
  403. self.fOscServer = None
  404. self.removeAllPlugins()
  405. self.host.lo_target = None
  406. self.host.lo_target_name = ""
  407. self.ui.act_file_refresh.setEnabled(False)
  408. # --------------------------------------------------------------------------------------------------------
  409. # Timers
  410. def startTimers(self):
  411. if self.fIdleTimerOSC == 0:
  412. self.fIdleTimerOSC = self.startTimer(20)
  413. HostWindow.startTimers(self)
  414. def restartTimersIfNeeded(self):
  415. if self.fIdleTimerOSC != 0:
  416. self.killTimer(self.fIdleTimerOSC)
  417. self.fIdleTimerOSC = self.startTimer(20)
  418. HostWindow.restartTimersIfNeeded(self)
  419. def killTimers(self):
  420. if self.fIdleTimerOSC != 0:
  421. self.killTimer(self.fIdleTimerOSC)
  422. self.fIdleTimerOSC = 0
  423. HostWindow.killTimers(self)
  424. # --------------------------------------------------------------------------------------------------------
  425. def removeAllPlugins(self):
  426. self.host.fPluginsInfo = []
  427. HostWindow.removeAllPlugins(self)
  428. # --------------------------------------------------------------------------------------------------------
  429. def loadSettings(self, firstTime):
  430. settings = HostWindow.loadSettings(self, firstTime)
  431. self.fOscAddress = settings.value("RemoteAddress", "osc.tcp://127.0.0.1:22752/Carla", type=str)
  432. def saveSettings(self):
  433. settings = HostWindow.saveSettings(self)
  434. if self.fOscAddress:
  435. settings.setValue("RemoteAddress", self.fOscAddress)
  436. # --------------------------------------------------------------------------------------------------------
  437. @pyqtSlot()
  438. def slot_fileConnect(self):
  439. dialog = QInputDialog(self)
  440. dialog.setInputMode(QInputDialog.TextInput)
  441. dialog.setLabelText(self.tr("Address:"))
  442. dialog.setTextValue(self.fOscAddress or "osc.tcp://127.0.0.1:22752/Carla")
  443. dialog.setWindowTitle(self.tr("Carla Control - Connect"))
  444. dialog.resize(400,1)
  445. ok = dialog.exec_()
  446. addr = dialog.textValue().strip()
  447. del dialog
  448. if not ok:
  449. return
  450. if not addr:
  451. return
  452. self.disconnectOsc()
  453. if addr:
  454. self.connectOsc(addr)
  455. @pyqtSlot()
  456. def slot_fileRefresh(self):
  457. if self.host.lo_target is None or self.fOscServer is None:
  458. return
  459. self.killTimers()
  460. self.removeAllPlugins()
  461. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  462. lo_send(self.host.lo_target, "/register", self.fOscServer.getFullURL())
  463. self.startTimers()
  464. @pyqtSlot()
  465. def slot_handleQuitCallback(self):
  466. HostWindow.slot_handleQuitCallback(self)
  467. self.disconnectOsc()
  468. # --------------------------------------------------------------------------------------------------------
  469. def timerEvent(self, event):
  470. if event.timerId() == self.fIdleTimerOSC:
  471. self.fOscServer.idle()
  472. if self.host.lo_target is None:
  473. self.disconnectOsc()
  474. HostWindow.timerEvent(self, event)
  475. def closeEvent(self, event):
  476. self.killTimers()
  477. if self.host.lo_target is not None and self.fOscServer is not None:
  478. lo_send(self.host.lo_target, "/unregister", self.fOscServer.getFullURL())
  479. HostWindow.closeEvent(self, event)
  480. # ------------------------------------------------------------------------------------------------------------