2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ----------------------------------------------------------------------------------------------------------------------
8 from qt_compat
import qt_config
11 from PyQt5
.QtCore
import QEventLoop
13 from PyQt6
.QtCore
import QEventLoop
15 # ------------------------------------------------------------------------------------------------------------
18 import ui_carla_osc_connect
20 from carla_backend_qt
import CarlaHostQtPlugin
21 from carla_host
import *
23 # ------------------------------------------------------------------------------------------------------------
37 from random
import random
39 # ------------------------------------------------------------------------------------------------------------
43 # ----------------------------------------------------------------------------------------------------------------------
46 class ConnectDialog(QDialog
):
47 def __init__(self
, parent
):
48 QDialog
.__init
__(self
, parent
)
49 self
.ui
= ui_carla_osc_connect
.Ui_Dialog()
52 self
.setWindowFlags(self
.windowFlags() & ~Qt
.WindowContextHelpButtonHint
)
54 # -------------------------------------------------------------------------------------------------------------
59 # -------------------------------------------------------------------------------------------------------------
62 self
.finished
.connect(self
.slot_saveSettings
)
63 self
.ui
.le_host
.textChanged
.connect(self
.slot_hostChanged
)
65 # -----------------------------------------------------------------------------------------------------------------
68 return (self
.ui
.le_host
.text(),
69 self
.ui
.sb_tcp_port
.value(),
70 self
.ui
.sb_udp_port
.value())
72 def checkIfButtonBoxShouldBeEnabled(self
, host
):
73 enabled
= len(host
) > 0
74 self
.ui
.buttonBox
.button(QDialogButtonBox
.Ok
).setEnabled(enabled
)
76 def loadSettings(self
):
77 settings
= QSafeSettings("falkTX", "CarlaOSCConnect")
79 self
.ui
.le_host
.setText(settings
.value("Host", "127.0.0.1", str))
80 self
.ui
.sb_tcp_port
.setValue(settings
.value("TCPPort", CARLA_DEFAULT_OSC_TCP_PORT_NUMBER
, int))
81 self
.ui
.sb_udp_port
.setValue(settings
.value("UDPPort", CARLA_DEFAULT_OSC_UDP_PORT_NUMBER
, int))
83 self
.checkIfButtonBoxShouldBeEnabled(self
.ui
.le_host
.text())
85 # ------------------------------------------------------------------------------------------------------------------
88 def slot_hostChanged(self
, text
):
89 self
.checkIfButtonBoxShouldBeEnabled(text
)
92 def slot_saveSettings(self
):
93 settings
= QSafeSettings("falkTX", "CarlaOSCConnect")
94 settings
.setValue("Host", self
.ui
.le_host
.text())
95 settings
.setValue("TCPPort", self
.ui
.sb_tcp_port
.value())
96 settings
.setValue("UDPPort", self
.ui
.sb_udp_port
.value())
98 # ------------------------------------------------------------------------------------------------------------------
101 QDialog
.done(self
, r
)
104 # ------------------------------------------------------------------------------------------------------------
107 class CarlaHostOSC(CarlaHostQtPlugin
):
109 CarlaHostQtPlugin
.__init
__(self
)
111 self
.lo_server_tcp
= None
112 self
.lo_server_udp
= None
113 self
.lo_target_tcp
= None
114 self
.lo_target_udp
= None
115 self
.lo_target_tcp_name
= ""
116 self
.lo_target_udp_name
= ""
118 self
.resetPendingMessages()
120 # -------------------------------------------------------------------
122 def resetPendingMessages(self
):
123 self
.lastMessageId
= 1
124 self
.pendingMessages
= []
127 def printAndReturnError(self
, error
):
129 self
.fLastError
= error
132 def sendMsg(self
, lines
):
134 return self
.printAndReturnError("not enough arguments")
136 method
= lines
.pop(0)
138 if method
== "set_engine_option":
141 if self
.lo_target_tcp
is None:
142 return self
.printAndReturnError("lo_target_tcp is None")
143 if self
.lo_target_tcp_name
is None:
144 return self
.printAndReturnError("lo_target_tcp_name is None")
146 if method
in ("clear_engine_xruns",
147 "cancel_engine_action",
151 #"clear_project_filename",
153 "patchbay_disconnect",
154 "patchbay_set_group_pos",
159 "transport_relocate",
162 "remove_all_plugins",
167 #"load_plugin_state",
168 #"save_plugin_state",
170 path
= "/ctrl/" + method
173 elif method
in (#"set_option",
181 "set_parameter_value",
182 "set_parameter_midi_channel",
183 "set_parameter_midi_cc",
190 #"randomize_parameters",
192 pluginId
= lines
.pop(0)
194 path
= "/%s/%i/%s" % (self
.lo_target_tcp_name
, pluginId
, method
)
196 elif method
== "send_midi_note":
197 pluginId
= lines
.pop(0)
199 channel
, note
, velocity
= lines
202 path
= "/%s/%i/note_on" % (self
.lo_target_tcp_name
, pluginId
)
204 path
= "/%s/%i/note_off" % (self
.lo_target_tcp_name
, pluginId
)
208 return self
.printAndReturnError("invalid method '%s'" % method
)
210 if len(self
.pendingMessages
) != 0:
211 return self
.printAndReturnError("A previous operation is still pending, please wait")
213 args
= [int(line
) if isinstance(line
, bool) else line
for line
in lines
]
217 lo_send(self
.lo_target_tcp
, path
, *args
)
220 messageId
= self
.lastMessageId
221 self
.lastMessageId
+= 1
222 self
.pendingMessages
.append(messageId
)
224 lo_send(self
.lo_target_tcp
, path
, messageId
, *args
)
226 while messageId
in self
.pendingMessages
:
227 QApplication
.processEvents(QEventLoop
.AllEvents
, 100)
229 error
= self
.responses
.pop(messageId
)
234 self
.fLastError
= error
237 def sendMsgAndSetError(self
, lines
):
238 return self
.sendMsg(lines
)
240 # -------------------------------------------------------------------
242 def engine_init(self
, driverName
, clientName
):
243 return self
.lo_target_tcp
is not None
245 def engine_close(self
):
248 def engine_idle(self
):
251 def is_engine_running(self
):
252 return self
.lo_target_tcp
is not None
254 def set_engine_about_to_close(self
):
257 # ---------------------------------------------------------------------------------------------------------------------
260 class CarlaControlServerTCP(Server
):
261 def __init__(self
, host
):
262 Server
.__init
__(self
, proto
=LO_TCP
)
265 host
= CarlaHostOSC()
270 self
.fReceivedMsgs
= False
272 while self
.recv(0) and self
.fReceivedMsgs
:
275 def getFullURL(self
):
276 return "%sctrl" % self
.get_url()
278 @make_method('/ctrl/cb', 'iiiiifs')
279 def carla_cb(self
, path
, args
):
280 if DEBUG
: print(path
, args
)
281 self
.fReceivedMsgs
= True
282 action
, pluginId
, value1
, value2
, value3
, valuef
, valueStr
= args
283 self
.host
._setViaCallback
(action
, pluginId
, value1
, value2
, value3
, valuef
, valueStr
)
284 engineCallback(self
.host
, action
, pluginId
, value1
, value2
, value3
, valuef
, valueStr
)
286 @make_method('/ctrl/info', 'iiiihiisssssss')
287 def carla_info(self
, path
, args
):
288 if DEBUG
: print(path
, args
)
289 self
.fReceivedMsgs
= True
291 pluginId
, type_
, category
, hints
, uniqueId
, optsAvail
, optsEnabled
,
292 name
, filename
, iconName
, realName
, label
, maker
, copyright
,
295 hints
&= ~PLUGIN_HAS_CUSTOM_UI
299 'category': category
,
301 'optionsAvailable': optsAvail
,
302 'optionsEnabled': optsEnabled
,
303 'uniqueId': uniqueId
,
304 'filename': filename
,
308 'copyright': copyright
,
312 self
.host
._set
_pluginInfoUpdate
(pluginId
, pinfo
)
313 self
.host
._set
_pluginRealName
(pluginId
, realName
)
315 @make_method('/ctrl/ports', 'iiiiiiii')
316 def carla_ports(self
, path
, args
):
317 if DEBUG
: print(path
, args
)
318 self
.fReceivedMsgs
= True
319 pluginId
, audioIns
, audioOuts
, midiIns
, midiOuts
, paramIns
, paramOuts
, paramTotal
= args
320 self
.host
._set
_audioCountInfo
(pluginId
, {'ins': audioIns
, 'outs': audioOuts
})
321 self
.host
._set
_midiCountInfo
(pluginId
, {'ins': midiOuts
, 'outs': midiOuts
})
322 self
.host
._set
_parameterCountInfo
(pluginId
, paramTotal
, {'ins': paramIns
, 'outs': paramOuts
})
324 @make_method('/ctrl/paramInfo', 'iissss')
325 def carla_paramInfo(self
, path
, args
):
326 if DEBUG
: print(path
, args
)
327 self
.fReceivedMsgs
= True
328 pluginId
, paramId
, name
, unit
, comment
, groupName
= args
335 'groupName': groupName
,
336 'scalePointCount': 0,
339 self
.host
._set
_parameterInfo
(pluginId
, paramId
, paramInfo
)
341 @make_method('/ctrl/paramData', 'iiiiiifff')
342 def carla_paramData(self
, path
, args
):
343 if DEBUG
: print(path
, args
)
344 self
.fReceivedMsgs
= True
345 pluginId
, paramId
, type_
, hints
, midiChan
, mappedCtrl
, mappedMin
, mappedMax
, value
= args
347 hints
&= ~
(PARAMETER_USES_SCALEPOINTS | PARAMETER_USES_CUSTOM_TEXT
)
354 'midiChannel': midiChan
,
355 'mappedControlIndex': mappedCtrl
,
356 'mappedMinimum': mappedMin
,
357 'mappedMaximum': mappedMax
,
359 self
.host
._set
_parameterData
(pluginId
, paramId
, paramData
)
360 self
.host
._set
_parameterValue
(pluginId
, paramId
, value
)
362 @make_method('/ctrl/paramRanges', 'iiffffff')
363 def carla_paramRanges(self
, path
, args
):
364 if DEBUG
: print(path
, args
)
365 self
.fReceivedMsgs
= True
366 pluginId
, paramId
, def_
, min_
, max_
, step
, stepSmall
, stepLarge
= args
373 'stepSmall': stepSmall
,
374 'stepLarge': stepLarge
,
376 self
.host
._set
_parameterRanges
(pluginId
, paramId
, paramRanges
)
378 @make_method('/ctrl/count', 'iiiiii')
379 def carla_count(self
, path
, args
):
380 if DEBUG
: print(path
, args
)
381 self
.fReceivedMsgs
= True
382 pluginId
, pcount
, mpcount
, cdcount
, cp
, cmp = args
383 self
.host
._set
_programCount
(pluginId
, pcount
)
384 self
.host
._set
_midiProgramCount
(pluginId
, mpcount
)
385 self
.host
._set
_customDataCount
(pluginId
, cdcount
)
386 self
.host
._set
_pluginInfoUpdate
(pluginId
, { 'programCurrent': cp
, 'midiProgramCurrent': cmp })
388 @make_method('/ctrl/pcount', 'iii')
389 def carla_pcount(self
, path
, args
):
390 if DEBUG
: print(path
, args
)
391 self
.fReceivedMsgs
= True
392 pluginId
, pcount
, mpcount
= args
393 self
.host
._set
_programCount
(pluginId
, pcount
)
394 self
.host
._set
_midiProgramCount
(pluginId
, mpcount
)
396 @make_method('/ctrl/prog', 'iis')
397 def carla_prog(self
, path
, args
):
398 if DEBUG
: print(path
, args
)
399 self
.fReceivedMsgs
= True
400 pluginId
, progId
, progName
= args
401 self
.host
._set
_programName
(pluginId
, progId
, progName
)
403 @make_method('/ctrl/mprog', 'iiiis')
404 def carla_mprog(self
, path
, args
):
405 if DEBUG
: print(path
, args
)
406 self
.fReceivedMsgs
= True
407 pluginId
, midiProgId
, bank
, program
, name
= args
408 self
.host
._set
_midiProgramData
(pluginId
, midiProgId
, {'bank': bank
, 'program': program
, 'name': name
})
410 @make_method('/ctrl/cdata', 'iisss')
411 def carla_cdata(self
, path
, args
):
412 if DEBUG
: print(path
, args
)
413 self
.fReceivedMsgs
= True
414 pluginId
, index
, type_
, key
, value
= args
415 self
.host
._set
_customData
(pluginId
, index
, { 'type': type_
, 'key': key
, 'value': value
})
417 @make_method('/ctrl/iparams', 'ifffffff')
418 def carla_iparams(self
, path
, args
):
419 if DEBUG
: print(path
, args
)
420 self
.fReceivedMsgs
= True
421 pluginId
, active
, drywet
, volume
, balLeft
, balRight
, pan
, ctrlChan
= args
422 self
.host
._set
_internalValue
(pluginId
, PARAMETER_ACTIVE
, active
)
423 self
.host
._set
_internalValue
(pluginId
, PARAMETER_DRYWET
, drywet
)
424 self
.host
._set
_internalValue
(pluginId
, PARAMETER_VOLUME
, volume
)
425 self
.host
._set
_internalValue
(pluginId
, PARAMETER_BALANCE_LEFT
, balLeft
)
426 self
.host
._set
_internalValue
(pluginId
, PARAMETER_BALANCE_RIGHT
, balRight
)
427 self
.host
._set
_internalValue
(pluginId
, PARAMETER_PANNING
, pan
)
428 self
.host
._set
_internalValue
(pluginId
, PARAMETER_CTRL_CHANNEL
, ctrlChan
)
430 @make_method('/ctrl/resp', 'is')
431 def carla_resp(self
, path
, args
):
432 if DEBUG
: print(path
, args
)
433 self
.fReceivedMsgs
= True
434 messageId
, error
= args
435 self
.host
.responses
[messageId
] = error
436 self
.host
.pendingMessages
.remove(messageId
)
438 @make_method('/ctrl/exit', '')
439 def carla_exit(self
, path
, args
):
440 if DEBUG
: print(path
, args
)
441 self
.fReceivedMsgs
= True
442 #self.host.lo_target_tcp = None
443 self
.host
.QuitCallback
.emit()
445 @make_method('/ctrl/exit-error', 's')
446 def carla_exit_error(self
, path
, args
):
447 if DEBUG
: print(path
, args
)
448 self
.fReceivedMsgs
= True
450 self
.host
.lo_target_tcp
= None
451 self
.host
.QuitCallback
.emit()
452 self
.host
.ErrorCallback
.emit(error
)
454 @make_method(None, None)
455 def fallback(self
, path
, args
):
456 print("ControlServerTCP::fallback(\"%s\") - unknown message, args =" % path
, args
)
457 self
.fReceivedMsgs
= True
459 # ---------------------------------------------------------------------------------------------------------------------
461 class CarlaControlServerUDP(Server
):
462 def __init__(self
, host
):
463 Server
.__init
__(self
, proto
=LO_UDP
)
466 host
= CarlaHostOSC()
471 self
.fReceivedMsgs
= False
473 while self
.recv(0) and self
.fReceivedMsgs
:
476 def getFullURL(self
):
477 return "%sctrl" % self
.get_url()
479 @make_method('/ctrl/runtime', 'fiihiiif')
480 def carla_runtime(self
, path
, args
):
481 self
.fReceivedMsgs
= True
482 load
, xruns
, playing
, frame
, bar
, beat
, tick
, bpm
= args
483 self
.host
._set
_runtime
_info
(load
, xruns
)
484 self
.host
._set
_transport
(bool(playing
), frame
, bar
, beat
, tick
, bpm
)
486 @make_method('/ctrl/param', 'iif')
487 def carla_param_fixme(self
, path
, args
):
488 self
.fReceivedMsgs
= True
489 pluginId
, paramId
, paramValue
= args
490 self
.host
._set
_parameterValue
(pluginId
, paramId
, paramValue
)
492 @make_method('/ctrl/peaks', 'iffff')
493 def carla_peaks(self
, path
, args
):
494 self
.fReceivedMsgs
= True
495 pluginId
, in1
, in2
, out1
, out2
= args
496 self
.host
._set
_peaks
(pluginId
, in1
, in2
, out1
, out2
)
498 @make_method(None, None)
499 def fallback(self
, path
, args
):
500 print("ControlServerUDP::fallback(\"%s\") - unknown message, args =" % path
, args
)
501 self
.fReceivedMsgs
= True
503 # ---------------------------------------------------------------------------------------------------------------------
506 class HostWindowOSC(HostWindow
):
507 def __init__(self
, host
, oscAddr
= None):
508 self
.fCustomOscAddress
= oscAddr
509 HostWindow
.__init
__(self
, host
, True)
513 # kdevelop likes this :)
514 host
= CarlaHostOSC()
517 # ----------------------------------------------------------------------------------------------------
518 # Connect actions to functions
520 self
.ui
.act_file_connect
.triggered
.connect(self
.slot_fileConnect
)
521 self
.ui
.act_file_refresh
.triggered
.connect(self
.slot_fileRefresh
)
523 # ----------------------------------------------------------------------------------------------------
527 QTimer
.singleShot(0, self
.connectOsc
)
529 def connectOsc(self
, addrTCP
= None, addrUDP
= None):
530 if self
.fCustomOscAddress
is not None:
531 addrTCP
= self
.fCustomOscAddress
.replace("osc.udp://","osc.tcp://")
532 addrUDP
= self
.fCustomOscAddress
.replace("osc.tcp://","osc.udp://")
535 if addrTCP
is not None:
536 self
.fOscAddressTCP
= addrTCP
537 if addrUDP
is not None:
538 self
.fOscAddressUDP
= addrUDP
540 lo_target_tcp_name
= addrTCP
.rsplit("/", 1)[-1]
541 lo_target_udp_name
= addrUDP
.rsplit("/", 1)[-1]
546 lo_target_tcp
= Address(addrTCP
)
547 lo_server_tcp
= CarlaControlServerTCP(self
.host
)
548 lo_send(lo_target_tcp
, "/register", lo_server_tcp
.getFullURL())
550 lo_target_udp
= Address(addrUDP
)
551 lo_server_udp
= CarlaControlServerUDP(self
.host
)
552 lo_send(lo_target_udp
, "/register", lo_server_udp
.getFullURL())
554 except AddressError
as e
:
562 fullError
= self
.tr("Failed to connect to the Carla instance.")
564 if len(err
.args
) > 0:
565 fullError
+= " %s\n%s\n" % (self
.tr("Error was:"), err
.args
[0])
568 fullError
+= self
.tr("Make sure the remote Carla is running and the URL and Port are correct.") + "\n"
569 fullError
+= self
.tr("If it still does not work, check your current device and the remote's firewall.")
571 CustomMessageBox(self
,
574 self
.tr("Connection failed"),
580 self
.host
.lo_server_tcp
= lo_server_tcp
581 self
.host
.lo_target_tcp
= lo_target_tcp
582 self
.host
.lo_target_tcp_name
= lo_target_tcp_name
584 self
.host
.lo_server_udp
= lo_server_udp
585 self
.host
.lo_target_udp
= lo_target_udp
586 self
.host
.lo_target_udp_name
= lo_target_udp_name
588 self
.ui
.act_file_refresh
.setEnabled(True)
592 def disconnectOsc(self
):
595 self
.removeAllPlugins()
598 self
.ui
.act_file_refresh
.setEnabled(False)
600 # --------------------------------------------------------------------------------------------------------
602 def unregister(self
):
603 if self
.host
.lo_server_tcp
is not None:
604 if self
.host
.lo_target_tcp
is not None:
606 lo_send(self
.host
.lo_target_tcp
, "/unregister", self
.host
.lo_server_tcp
.getFullURL())
609 self
.host
.lo_target_tcp
= None
611 while self
.host
.lo_server_tcp
.recv(0):
613 #self.host.lo_server_tcp.free()
614 self
.host
.lo_server_tcp
= None
616 if self
.host
.lo_server_udp
is not None:
617 if self
.host
.lo_target_udp
is not None:
619 lo_send(self
.host
.lo_target_udp
, "/unregister", self
.host
.lo_server_udp
.getFullURL())
622 self
.host
.lo_target_udp
= None
624 while self
.host
.lo_server_udp
.recv(0):
626 #self.host.lo_server_udp.free()
627 self
.host
.lo_server_udp
= None
629 self
.host
.lo_target_tcp_name
= ""
630 self
.host
.lo_target_udp_name
= ""
632 # --------------------------------------------------------------------------------------------------------
636 HostWindow
.idleFast(self
)
638 if self
.host
.lo_server_tcp
is not None:
639 self
.host
.lo_server_tcp
.idle()
643 if self
.host
.lo_server_udp
is not None:
644 self
.host
.lo_server_udp
.idle()
648 # --------------------------------------------------------------------------------------------------------
650 def removeAllPlugins(self
):
651 self
.host
.fPluginsInfo
= {}
652 HostWindow
.removeAllPlugins(self
)
654 # --------------------------------------------------------------------------------------------------------
656 def loadSettings(self
, firstTime
):
657 settings
= HostWindow
.loadSettings(self
, firstTime
)
658 if self
.fCustomOscAddress
is not None:
659 self
.fOscAddressTCP
= settings
.value("RemoteAddressTCP", "osc.tcp://127.0.0.1:22752/Carla", str)
660 self
.fOscAddressUDP
= settings
.value("RemoteAddressUDP", "osc.udp://127.0.0.1:22752/Carla", str)
662 def saveSettings(self
):
663 settings
= HostWindow
.saveSettings(self
)
664 if self
.fOscAddressTCP
:
665 settings
.setValue("RemoteAddressTCP", self
.fOscAddressTCP
)
666 if self
.fOscAddressUDP
:
667 settings
.setValue("RemoteAddressUDP", self
.fOscAddressUDP
)
669 # --------------------------------------------------------------------------------------------------------
672 def slot_fileConnect(self
):
673 dialog
= ConnectDialog(self
)
675 if not dialog
.exec_():
678 host
, tcpPort
, udpPort
= dialog
.getResult()
681 self
.connectOsc("osc.tcp://%s:%i/Carla" % (host
, tcpPort
),
682 "osc.udp://%s:%i/Carla" % (host
, udpPort
))
685 def slot_fileRefresh(self
):
686 if None in (self
.host
.lo_server_tcp
, self
.host
.lo_server_udp
, self
.host
.lo_target_tcp
, self
.host
.lo_target_udp
):
689 lo_send(self
.host
.lo_target_udp
, "/unregister", self
.host
.lo_server_udp
.getFullURL())
690 while self
.host
.lo_server_udp
.recv(0):
692 #self.host.lo_server_udp.free()
694 lo_send(self
.host
.lo_target_tcp
, "/unregister", self
.host
.lo_server_tcp
.getFullURL())
695 while self
.host
.lo_server_tcp
.recv(0):
697 #self.host.lo_server_tcp.free()
699 self
.removeAllPlugins()
702 self
.host
.lo_server_tcp
= CarlaControlServerTCP(self
.host
)
703 self
.host
.lo_server_udp
= CarlaControlServerUDP(self
.host
)
706 lo_send(self
.host
.lo_target_tcp
, "/register", self
.host
.lo_server_tcp
.getFullURL())
712 lo_send(self
.host
.lo_target_udp
, "/register", self
.host
.lo_server_udp
.getFullURL())
717 # --------------------------------------------------------------------------------------------------------
720 def slot_handleSIGTERM(self
):
721 print("Got SIGTERM -> Closing now")
722 self
.host
.pendingMessages
= []
726 def slot_handleQuitCallback(self
):
728 HostWindow
.slot_handleQuitCallback(self
)
730 # --------------------------------------------------------------------------------------------------------
732 def closeEvent(self
, event
):
736 HostWindow
.closeEvent(self
, event
)
738 # ------------------------------------------------------------------------------------------------------------