Extend REAPER extension to read/write automation items, wire desktop app to it over UDP
extension-reaper-macos: split main.cpp into tracking.cpp (polls selected envelope/automation items, reads/writes D_BASELINE) and socket.cpp (basic UDP listener, non-blocking, drains all pending packets per tick). Nudging now applies to every selected automation item across every track in the project, not just the first one found. app-desktop-macos: new "Extension" column (mirrors the OSC/Remote columns) to enable/disable the UDP connection and point it at a host/port, plus a log window. FocusFaderWidget gets an OSC/Ext output-mode toggle (mirrors TransportWidget's MIDI/OSC toggle) so a fader can drive the extension directly instead of/alongside OSC. Also renamed the Remote/Tablet buttons for clarity and split their status indicator into its own column. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import socket
|
||||
|
||||
_ext_socket = None
|
||||
_ext_ip = "127.0.0.1"
|
||||
_ext_port = 9124
|
||||
_ext_enabled = False
|
||||
|
||||
|
||||
def get_extension_socket():
|
||||
global _ext_socket
|
||||
|
||||
if _ext_socket is None:
|
||||
_ext_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
return _ext_socket
|
||||
|
||||
|
||||
def set_extension_target(ip=None, port=None):
|
||||
global _ext_socket, _ext_ip, _ext_port
|
||||
|
||||
if ip is not None:
|
||||
_ext_ip = ip
|
||||
|
||||
if port is not None:
|
||||
_ext_port = port
|
||||
|
||||
_ext_socket = None
|
||||
|
||||
|
||||
def set_extension_enabled(enabled):
|
||||
global _ext_enabled
|
||||
_ext_enabled = enabled
|
||||
|
||||
|
||||
def is_extension_enabled():
|
||||
return _ext_enabled
|
||||
|
||||
|
||||
def send_to_extension(value):
|
||||
"""Sends a plain numeric delta to extension-reaper-macos over UDP.
|
||||
No-op (kill switch) unless set_extension_enabled(True) has been called."""
|
||||
if not _ext_enabled:
|
||||
return
|
||||
try:
|
||||
get_extension_socket().sendto(str(value).encode(), (_ext_ip, _ext_port))
|
||||
except Exception as e:
|
||||
print(f"[Extension send] Error: {e}")
|
||||
@@ -19,6 +19,7 @@ from zonebutton import ZoneButton
|
||||
from colorswatchpopup import ColorSwatchPopup
|
||||
from osc_sender import send_osc_message
|
||||
from midi_sender import send_cc
|
||||
from extension_sender import send_to_extension
|
||||
|
||||
|
||||
class FocusFaderWidget(QWidget):
|
||||
@@ -96,6 +97,29 @@ class FocusFaderWidget(QWidget):
|
||||
_grp2_layout.setContentsMargins(4, 4, 4, 4)
|
||||
_grp2_layout.setSpacing(3)
|
||||
|
||||
# OSC / Ext mode selector
|
||||
mode_row = QHBoxLayout()
|
||||
mode_row.setContentsMargins(0, 0, 0, 0)
|
||||
mode_row.setSpacing(2)
|
||||
self.osc_mode_btn = QPushButton("OSC")
|
||||
self.osc_mode_btn.setFixedHeight(18)
|
||||
self.osc_mode_btn.setCheckable(True)
|
||||
self.osc_mode_btn.setChecked(True)
|
||||
self.osc_mode_btn.setStyleSheet("border: none;")
|
||||
self.ext_mode_btn = QPushButton("Ext")
|
||||
self.ext_mode_btn.setFixedHeight(18)
|
||||
self.ext_mode_btn.setCheckable(True)
|
||||
self.ext_mode_btn.setChecked(False)
|
||||
self.ext_mode_btn.setStyleSheet("border: none;")
|
||||
self.osc_mode_btn.clicked.connect(lambda: self.set_output_mode("osc"))
|
||||
self.ext_mode_btn.clicked.connect(lambda: self.set_output_mode("ext"))
|
||||
mode_row.addWidget(self.osc_mode_btn)
|
||||
mode_row.addWidget(self.ext_mode_btn)
|
||||
_grp2_layout.addLayout(mode_row)
|
||||
|
||||
self.output_mode = "osc"
|
||||
self._last_ext_value = None # tracks previous fader value, for delta-on-move in Ext mode
|
||||
|
||||
# OSC address field
|
||||
self.osc_addr_out_edit = QLineEdit("/track/volume")
|
||||
self.osc_addr_out_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter)
|
||||
@@ -376,6 +400,24 @@ class FocusFaderWidget(QWidget):
|
||||
self.touch_sense_mode = "jl_cooper" if index == 1 else "off"
|
||||
self.is_touching = False
|
||||
|
||||
def set_output_mode(self, mode):
|
||||
self.output_mode = mode
|
||||
if mode == "osc":
|
||||
self.osc_mode_btn.setChecked(True)
|
||||
self.ext_mode_btn.setChecked(False)
|
||||
self.osc_mode_btn.setStyleSheet("font-weight: bold; color: #00e676;")
|
||||
self.ext_mode_btn.setStyleSheet("")
|
||||
self.osc_addr_out_edit.setVisible(True)
|
||||
self.cc_output_lbl.setText(self._osc_output_label(self.fader.value()))
|
||||
else:
|
||||
self.ext_mode_btn.setChecked(True)
|
||||
self.osc_mode_btn.setChecked(False)
|
||||
self.ext_mode_btn.setStyleSheet("font-weight: bold; color: #00e676;")
|
||||
self.osc_mode_btn.setStyleSheet("")
|
||||
self.osc_addr_out_edit.setVisible(False)
|
||||
self.cc_output_lbl.setText("Ext [--]")
|
||||
self._last_ext_value = None # reset delta tracking on mode switch
|
||||
|
||||
def receive_osc_value(self, value):
|
||||
"""Update the fader to reflect feedback from the DAW, without echoing it back out."""
|
||||
if self.is_touching:
|
||||
@@ -449,8 +491,11 @@ class FocusFaderWidget(QWidget):
|
||||
self._set_readout_style("clear")
|
||||
|
||||
def on_fader_moved(self, value):
|
||||
addr = self.osc_addr_out_edit.text().strip() or "/track/volume"
|
||||
self.cc_output_lbl.setText(self._osc_output_label(value))
|
||||
if self.output_mode == "ext":
|
||||
self.cc_output_lbl.setText(f"Ext [{value}]")
|
||||
else:
|
||||
self.cc_output_lbl.setText(self._osc_output_label(value))
|
||||
|
||||
if self.pickup_mode and self.hunting:
|
||||
crossed = (
|
||||
(self.prev_position < self.last_sent <= value) or
|
||||
@@ -462,17 +507,32 @@ class FocusFaderWidget(QWidget):
|
||||
self._stop_blink()
|
||||
self._set_readout_style("orange")
|
||||
self.last_sent = value
|
||||
send_osc_message(addr, value / 127.0)
|
||||
self._send_hw_feedback(value)
|
||||
self._emit_value(value)
|
||||
else:
|
||||
self.readout.setText(f"{addr} → {value}")
|
||||
self.readout.setText(f"[{value}]")
|
||||
return
|
||||
else:
|
||||
self.last_sent = value
|
||||
self.prev_position = value
|
||||
self._emit_value(value)
|
||||
self.readout.setText(f"[{value}]")
|
||||
|
||||
def _emit_value(self, value):
|
||||
"""Sends the current fader value out via whichever protocol is
|
||||
selected. Ext mode sends a delta (change since last move), not the
|
||||
absolute position — the extension's UDP protocol only understands
|
||||
deltas added to the current baseline, not absolute targets."""
|
||||
if self.output_mode == "ext":
|
||||
if self._last_ext_value is None:
|
||||
self._last_ext_value = value
|
||||
delta = (value - self._last_ext_value) / 127.0
|
||||
self._last_ext_value = value
|
||||
if delta != 0:
|
||||
send_to_extension(delta)
|
||||
else:
|
||||
addr = self.osc_addr_out_edit.text().strip() or "/track/volume"
|
||||
send_osc_message(addr, value / 127.0)
|
||||
self._send_hw_feedback(value)
|
||||
self.readout.setText(f"{addr} → {value}")
|
||||
self._send_hw_feedback(value)
|
||||
|
||||
def _send_hw_feedback(self, value):
|
||||
"""Sends a translated MIDI CC out (e.g. so a motorized fader's position
|
||||
@@ -558,6 +618,7 @@ class FocusFaderWidget(QWidget):
|
||||
"hw_out_cc": self.hw_cc_spin.value(),
|
||||
"hw_out_ch": self.hw_ch_spin.value(),
|
||||
"touch_sense_mode": self.touch_sense_mode,
|
||||
"output_mode": self.output_mode,
|
||||
}
|
||||
|
||||
def set_state(self, state):
|
||||
@@ -599,3 +660,4 @@ class FocusFaderWidget(QWidget):
|
||||
self.touch_sense_combo.blockSignals(False)
|
||||
self.is_touching = False
|
||||
self._update_hw_visibility()
|
||||
self.set_output_mode(state.get("output_mode", "osc"))
|
||||
|
||||
+265
-27
@@ -75,6 +75,13 @@ from osc_sender import (
|
||||
set_osc_target,
|
||||
)
|
||||
|
||||
from extension_sender import (
|
||||
send_to_extension,
|
||||
set_extension_target,
|
||||
set_extension_enabled,
|
||||
is_extension_enabled,
|
||||
)
|
||||
|
||||
from ws_server import WSServer
|
||||
|
||||
def set_window(w):
|
||||
@@ -355,6 +362,67 @@ picker_transport_out.setFixedWidth(200)
|
||||
vstack_osc = QVBoxLayout()
|
||||
vstack_osc.setSpacing(2)
|
||||
|
||||
# Layout: vstack_remote — Remote's own self-contained column (indicator,
|
||||
# address, buttons), placed beside vstack_osc rather than mixed into it, so
|
||||
# the Remote status rows align above the Remote buttons, not OSC's.
|
||||
vstack_remote = QVBoxLayout()
|
||||
vstack_remote.setSpacing(2)
|
||||
|
||||
# Layout: hstack_remote_buttons
|
||||
hstack_remote_buttons = QHBoxLayout()
|
||||
|
||||
# Layout: hstack_osc_and_remote — holds OSC, Remote, and Extension columns
|
||||
# side by side (name kept from when it only held two, to avoid churn)
|
||||
hstack_osc_and_remote = QHBoxLayout()
|
||||
hstack_osc_and_remote.setSpacing(16)
|
||||
|
||||
# Layout: vstack_extension — Extension's own column, mirrors vstack_osc's
|
||||
# row structure exactly (activity, address, port, buttons — no stretch
|
||||
# needed since it has the same number of rows as OSC's column).
|
||||
vstack_extension = QVBoxLayout()
|
||||
vstack_extension.setSpacing(2)
|
||||
|
||||
# Layout: hstack_extension_activity_and_label
|
||||
hstack_extension_activity_and_label = QHBoxLayout()
|
||||
|
||||
# Layout: hstack_extension_ip
|
||||
hstack_extension_ip = QHBoxLayout()
|
||||
|
||||
# Layout: hstack_extension_port
|
||||
hstack_extension_port = QHBoxLayout()
|
||||
|
||||
# Layout: hstack_extension_buttons
|
||||
hstack_extension_buttons = QHBoxLayout()
|
||||
|
||||
# Widget: activity_indicator_extension
|
||||
activity_indicator_extension = QLabel("●")
|
||||
activity_indicator_extension.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;")
|
||||
|
||||
# Widget: label_connection_extension
|
||||
label_connection_extension = QLabel("Not Connected")
|
||||
label_connection_extension.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;")
|
||||
|
||||
# Widget: textbox_extension_ip
|
||||
textbox_extension_ip = QLineEdit("127.0.0.1")
|
||||
textbox_extension_ip.setFixedWidth(110)
|
||||
textbox_extension_ip.setPlaceholderText("127.0.0.1")
|
||||
|
||||
# Widget: spinner_extension_port
|
||||
spinner_extension_port = QSpinBox()
|
||||
spinner_extension_port.setMinimum(1024)
|
||||
spinner_extension_port.setMaximum(65535)
|
||||
spinner_extension_port.setValue(9124)
|
||||
spinner_extension_port.setFixedWidth(65)
|
||||
|
||||
# Widget: button_extension_toggle
|
||||
button_extension_toggle = QPushButton("Connect Extension")
|
||||
button_extension_toggle.setFixedWidth(130)
|
||||
|
||||
# Widget: button_extension_log
|
||||
button_extension_log = QPushButton("Log")
|
||||
button_extension_log.setFixedWidth(65)
|
||||
button_extension_log.setCheckable(True)
|
||||
|
||||
# Layout: hstack_osc_activity_and_label
|
||||
hstack_osc_activity_and_label = QHBoxLayout()
|
||||
|
||||
@@ -399,26 +467,40 @@ button_osc_toggle = QPushButton("Start OSC")
|
||||
button_osc_toggle.setFixedWidth(90)
|
||||
|
||||
# Widget: button_tablet_server
|
||||
button_tablet_server = QPushButton("Start Tablet")
|
||||
button_tablet_server.setFixedWidth(90)
|
||||
button_tablet_server = QPushButton("Enable Remote")
|
||||
button_tablet_server.setFixedWidth(110)
|
||||
|
||||
# Widget: button_tablet_log
|
||||
button_tablet_log = QPushButton("Tablet")
|
||||
button_tablet_log.setFixedWidth(65)
|
||||
button_tablet_log = QPushButton("Remote Log")
|
||||
button_tablet_log.setFixedWidth(85)
|
||||
button_tablet_log.setCheckable(True)
|
||||
|
||||
# Widget: button_ws_log
|
||||
button_ws_log = QPushButton("WS")
|
||||
button_ws_log.setFixedWidth(50)
|
||||
button_ws_log = QPushButton("WS Log")
|
||||
button_ws_log.setFixedWidth(65)
|
||||
button_ws_log.setCheckable(True)
|
||||
|
||||
# Widget: label_tablet_status
|
||||
label_tablet_status = QLabel("")
|
||||
label_tablet_status.setStyleSheet("color: #8e24aa; font-size: 11px; font-family: monospace;")
|
||||
|
||||
# Layout: hstack_remote_activity_and_label
|
||||
hstack_remote_activity_and_label = QHBoxLayout()
|
||||
|
||||
# Layout: hstack_remote_ip
|
||||
hstack_remote_ip = QHBoxLayout()
|
||||
|
||||
# Widget: activity_indicator_remote
|
||||
activity_indicator_remote = QLabel("●")
|
||||
activity_indicator_remote.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;")
|
||||
|
||||
# Widget: label_connection_remote
|
||||
label_connection_remote = QLabel("Not Listening")
|
||||
label_connection_remote.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;")
|
||||
|
||||
# Widget: button_osc_messages_log
|
||||
button_osc_messages_log = QPushButton("Messages")
|
||||
button_osc_messages_log.setFixedWidth(80)
|
||||
button_osc_messages_log = QPushButton("OSC/MIDI Log")
|
||||
button_osc_messages_log.setFixedWidth(100)
|
||||
button_osc_messages_log.setCheckable(True)
|
||||
|
||||
# Widget: button_layout_panel
|
||||
@@ -722,6 +804,34 @@ textedit_tablet_log.setStyleSheet("""
|
||||
}
|
||||
""")
|
||||
|
||||
# Container: container_extension_log
|
||||
container_extension_log = QWidget(None, Qt.WindowType.Window)
|
||||
container_extension_log.setWindowTitle("Extension")
|
||||
container_extension_log.setStyleSheet("background-color: #1a1a1a;")
|
||||
container_extension_log.setFixedWidth(360)
|
||||
|
||||
vstack_extension_log = QVBoxLayout(container_extension_log)
|
||||
vstack_extension_log.setContentsMargins(6, 6, 6, 6)
|
||||
vstack_extension_log.setSpacing(4)
|
||||
|
||||
hstack_extension_log_header = QHBoxLayout()
|
||||
label_extension_log_title = QLabel("Extension")
|
||||
label_extension_log_title.setStyleSheet("color: #00bcd4; font-weight: bold; font-size: 11px;")
|
||||
button_extension_log_clear = QPushButton("Clear")
|
||||
button_extension_log_clear.setFixedSize(45, 18)
|
||||
|
||||
textedit_extension_log = QTextEdit()
|
||||
textedit_extension_log.setReadOnly(True)
|
||||
textedit_extension_log.setStyleSheet("""
|
||||
QTextEdit {
|
||||
background-color: #111111;
|
||||
color: #00bcd4;
|
||||
font-family: monospace;
|
||||
font-size: 10px;
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
# MARK: LAYOUT PANEL
|
||||
container_layout_panel = QWidget(None, Qt.WindowType.Window)
|
||||
container_layout_panel.setWindowTitle("Layout")
|
||||
@@ -1263,24 +1373,73 @@ class MainWindow(QMainWindow):
|
||||
hstack_osc_ports.addWidget(spinner_osc_send_port)
|
||||
hstack_osc_ports.addStretch()
|
||||
|
||||
# build hstack for control buttons
|
||||
# build hstack for control buttons (OSC's own only — Remote's live
|
||||
# in hstack_remote_buttons instead, in their own column)
|
||||
hstack_osc_buttons.addWidget(button_osc_toggle)
|
||||
hstack_osc_buttons.addWidget(button_osc_messages_log)
|
||||
hstack_osc_buttons.addWidget(button_layout_panel)
|
||||
hstack_osc_buttons.addWidget(button_focus_panel)
|
||||
hstack_osc_buttons.addSpacing(16)
|
||||
hstack_osc_buttons.addWidget(button_tablet_server)
|
||||
hstack_osc_buttons.addWidget(button_tablet_log)
|
||||
hstack_osc_buttons.addWidget(button_ws_log)
|
||||
hstack_osc_buttons.addWidget(label_tablet_status)
|
||||
hstack_osc_buttons.addStretch()
|
||||
|
||||
# add all rows to vstack
|
||||
# build hstack with activity indicator and connection label (Remote)
|
||||
hstack_remote_activity_and_label.addWidget(activity_indicator_remote)
|
||||
hstack_remote_activity_and_label.addWidget(label_connection_remote)
|
||||
hstack_remote_activity_and_label.addStretch()
|
||||
|
||||
# build hstack for the remote's ws:// address
|
||||
hstack_remote_ip.addWidget(label_tablet_status)
|
||||
hstack_remote_ip.addStretch()
|
||||
|
||||
# build hstack for the remote's own buttons
|
||||
hstack_remote_buttons.addWidget(button_tablet_server)
|
||||
hstack_remote_buttons.addWidget(button_tablet_log)
|
||||
hstack_remote_buttons.addWidget(button_ws_log)
|
||||
hstack_remote_buttons.addStretch()
|
||||
|
||||
# add OSC's rows to its own vstack
|
||||
vstack_osc.addLayout(hstack_osc_activity_and_label) # activity indicator and connection label
|
||||
vstack_osc.addLayout(hstack_osc_ip) # DAW IP input
|
||||
vstack_osc.addLayout(hstack_osc_ports) # listen and send ports
|
||||
vstack_osc.addLayout(hstack_osc_buttons) # control buttons
|
||||
|
||||
# add Remote's rows to its own vstack, so its indicator/address sit
|
||||
# directly above its own buttons, not OSC's
|
||||
vstack_remote.addLayout(hstack_remote_activity_and_label) # remote activity indicator and connection label
|
||||
vstack_remote.addLayout(hstack_remote_ip) # remote ws:// address
|
||||
vstack_remote.addStretch()
|
||||
vstack_remote.addLayout(hstack_remote_buttons) # remote control buttons
|
||||
|
||||
# build hstack with activity indicator and connection label (Extension)
|
||||
hstack_extension_activity_and_label.addWidget(activity_indicator_extension)
|
||||
hstack_extension_activity_and_label.addWidget(label_connection_extension)
|
||||
hstack_extension_activity_and_label.addStretch()
|
||||
|
||||
# build hstack for the extension's target IP
|
||||
hstack_extension_ip.addWidget(QLabel("IP"))
|
||||
hstack_extension_ip.addWidget(textbox_extension_ip)
|
||||
hstack_extension_ip.addStretch()
|
||||
|
||||
# build hstack for the extension's target port
|
||||
hstack_extension_port.addWidget(QLabel("Port"))
|
||||
hstack_extension_port.addWidget(spinner_extension_port)
|
||||
hstack_extension_port.addStretch()
|
||||
|
||||
# build hstack for the extension's own buttons
|
||||
hstack_extension_buttons.addWidget(button_extension_toggle)
|
||||
hstack_extension_buttons.addWidget(button_extension_log)
|
||||
hstack_extension_buttons.addStretch()
|
||||
|
||||
# add Extension's rows to its own vstack
|
||||
vstack_extension.addLayout(hstack_extension_activity_and_label)
|
||||
vstack_extension.addLayout(hstack_extension_ip)
|
||||
vstack_extension.addLayout(hstack_extension_port)
|
||||
vstack_extension.addLayout(hstack_extension_buttons)
|
||||
|
||||
# place OSC, Remote, and Extension columns side by side
|
||||
hstack_osc_and_remote.addLayout(vstack_osc)
|
||||
hstack_osc_and_remote.addLayout(vstack_remote)
|
||||
hstack_osc_and_remote.addLayout(vstack_extension)
|
||||
|
||||
# connect signals
|
||||
textbox_osc_daw_ip.textChanged.connect(update_osc_server_ip)
|
||||
spinner_osc_send_port.valueChanged.connect(update_osc_server_port)
|
||||
@@ -1290,11 +1449,8 @@ class MainWindow(QMainWindow):
|
||||
button_layout_panel.clicked.connect(layout_panel_show_hide)
|
||||
button_focus_panel.clicked.connect(focus_panel_show_hide)
|
||||
|
||||
# add the combined stack to the row
|
||||
hstack_io_row.addLayout(vstack_osc)
|
||||
|
||||
# add the combined stack to the row
|
||||
hstack_io_row.addLayout(vstack_osc)
|
||||
# add the combined OSC + Remote stack to the row
|
||||
hstack_io_row.addLayout(hstack_osc_and_remote)
|
||||
|
||||
hstack_io_row.addStretch()
|
||||
main_layout.addLayout(hstack_io_row)
|
||||
@@ -1437,6 +1593,18 @@ class MainWindow(QMainWindow):
|
||||
button_tablet_log.clicked.connect(tablet_log_show_hide)
|
||||
button_ws_log.clicked.connect(ws_log_show_hide)
|
||||
|
||||
#MARK: EXTENSION LOG CONTAINER
|
||||
hstack_extension_log_header.addWidget(label_extension_log_title)
|
||||
hstack_extension_log_header.addStretch()
|
||||
hstack_extension_log_header.addWidget(button_extension_log_clear)
|
||||
vstack_extension_log.addLayout(hstack_extension_log_header)
|
||||
vstack_extension_log.addWidget(textedit_extension_log)
|
||||
button_extension_log_clear.clicked.connect(lambda: textedit_extension_log.clear())
|
||||
container_extension_log.setVisible(False)
|
||||
|
||||
button_extension_toggle.clicked.connect(toggle_extension_connection)
|
||||
button_extension_log.clicked.connect(extension_log_show_hide)
|
||||
|
||||
#MARK: LAYOUT PANEL
|
||||
container_layout_panel.setVisible(True)
|
||||
button_layout_panel.setChecked(True)
|
||||
@@ -1714,6 +1882,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
QTimer.singleShot(0, _auto_start_osc_server)
|
||||
QTimer.singleShot(0, _auto_start_tablet_server)
|
||||
QTimer.singleShot(0, _auto_start_extension)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
@@ -2632,6 +2801,9 @@ def save_io_config():
|
||||
# server has had a chance to auto-start.
|
||||
"osc_running": loaded_presets.get("io_config", {}).get("osc_running", True),
|
||||
"tablet_running": loaded_presets.get("io_config", {}).get("tablet_running", True),
|
||||
"extension_ip": textbox_extension_ip.text(),
|
||||
"extension_port": spinner_extension_port.value(),
|
||||
"extension_running": loaded_presets.get("io_config", {}).get("extension_running", True),
|
||||
}
|
||||
save_presets_file(loaded_presets)
|
||||
|
||||
@@ -2643,6 +2815,10 @@ def save_tablet_running_state(running):
|
||||
loaded_presets.setdefault("io_config", {})["tablet_running"] = running
|
||||
save_io_config()
|
||||
|
||||
def save_extension_running_state(running):
|
||||
loaded_presets.setdefault("io_config", {})["extension_running"] = running
|
||||
save_io_config()
|
||||
|
||||
def load_io_config():
|
||||
global available_ports_midi_in
|
||||
global available_ports_midi_controller_in
|
||||
@@ -2668,6 +2844,12 @@ def load_io_config():
|
||||
send = cfg.get("osc_send_port", 0)
|
||||
if send:
|
||||
spinner_osc_send_port.setValue(send)
|
||||
ext_ip = cfg.get("extension_ip", "")
|
||||
if ext_ip:
|
||||
textbox_extension_ip.setText(ext_ip)
|
||||
ext_port = cfg.get("extension_port", 0)
|
||||
if ext_port:
|
||||
spinner_extension_port.setValue(ext_port)
|
||||
|
||||
def preset_build_snapshot(name):
|
||||
save_transport_preset()
|
||||
@@ -3452,6 +3634,55 @@ def toggle_tablet_server():
|
||||
_start_tablet_server()
|
||||
|
||||
|
||||
def log_extension(line):
|
||||
from datetime import datetime
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
full = f"<span style='color:#555'>{ts}</span> <span style='color:#00bcd4'>{line}</span>"
|
||||
_append_log(textedit_extension_log, full)
|
||||
|
||||
|
||||
def extension_log_show_hide(checked):
|
||||
container_extension_log.setVisible(checked)
|
||||
if checked:
|
||||
extension_log_window_calc()
|
||||
|
||||
|
||||
def extension_log_window_calc():
|
||||
geo = app_window.frameGeometry()
|
||||
container_extension_log.setFixedWidth(360)
|
||||
container_extension_log.setFixedHeight(geo.height())
|
||||
container_extension_log.move(geo.right() + 1096, geo.top())
|
||||
|
||||
|
||||
def toggle_extension_connection():
|
||||
if is_extension_enabled():
|
||||
set_extension_enabled(False)
|
||||
button_extension_toggle.setText("Connect Extension")
|
||||
label_connection_extension.setText("Not Connected")
|
||||
label_connection_extension.setStyleSheet("color: #888;")
|
||||
activity_indicator_extension.setStyleSheet("color: #444; font-size: 14px;")
|
||||
log_extension("disconnected")
|
||||
save_extension_running_state(False)
|
||||
else:
|
||||
ip = textbox_extension_ip.text().strip() or "127.0.0.1"
|
||||
port = spinner_extension_port.value()
|
||||
set_extension_target(ip=ip, port=port)
|
||||
set_extension_enabled(True)
|
||||
button_extension_toggle.setText("Disconnect Extension")
|
||||
label_connection_extension.setText(f"Connected :{port}")
|
||||
label_connection_extension.setStyleSheet("color: #27ae60;")
|
||||
activity_indicator_extension.setStyleSheet("color: #2ecc71; font-size: 14px;")
|
||||
log_extension(f"connected {ip}:{port}")
|
||||
send_to_extension(0.0) # harmless no-op nudge, just proves reachability
|
||||
save_extension_running_state(True)
|
||||
|
||||
|
||||
def _auto_start_extension():
|
||||
should_connect = loaded_presets.get("io_config", {}).get("extension_running", True)
|
||||
if should_connect:
|
||||
toggle_extension_connection()
|
||||
|
||||
|
||||
def _start_tablet_server():
|
||||
global ws_server
|
||||
import socket as _socket
|
||||
@@ -3470,8 +3701,11 @@ def _start_tablet_server():
|
||||
s.close()
|
||||
except Exception:
|
||||
ip = "localhost"
|
||||
button_tablet_server.setText("Stop Tablet")
|
||||
button_tablet_server.setText("Disable Remote")
|
||||
label_tablet_status.setText(f"ws://{ip}:8765")
|
||||
label_connection_remote.setText("Listening :8765")
|
||||
label_connection_remote.setStyleSheet("color: #27ae60;")
|
||||
activity_indicator_remote.setStyleSheet("color: #2ecc71; font-size: 14px;")
|
||||
log_tablet(f"WS ws://{ip}:8765")
|
||||
save_tablet_running_state(True)
|
||||
if ip != "localhost":
|
||||
@@ -3489,9 +3723,12 @@ def _stop_tablet_server():
|
||||
if ws_server:
|
||||
ws_server.stop()
|
||||
ws_server = None
|
||||
button_tablet_server.setText("Start Tablet")
|
||||
button_tablet_server.setText("Enable Remote")
|
||||
save_tablet_running_state(False)
|
||||
label_tablet_status.setText("")
|
||||
label_connection_remote.setText("Not Listening")
|
||||
label_connection_remote.setStyleSheet("color: #888;")
|
||||
activity_indicator_remote.setStyleSheet("color: #444; font-size: 14px;")
|
||||
log_tablet("server stopped")
|
||||
_stop_bonjour_advertisement()
|
||||
|
||||
@@ -3663,17 +3900,18 @@ def _on_focus_fader_control_f(uid, value):
|
||||
if f.uid != uid:
|
||||
continue
|
||||
value = max(0.0, min(1.0, value))
|
||||
addr = f.osc_addr_out_edit.text().strip() or "/track/volume"
|
||||
slider_value = round(value * 127)
|
||||
f.fader.blockSignals(True)
|
||||
f.fader.setValue(slider_value)
|
||||
f.fader.blockSignals(False)
|
||||
f.last_sent = slider_value
|
||||
f.prev_position = slider_value
|
||||
f.cc_output_lbl.setText(f._osc_output_label(slider_value))
|
||||
f.readout.setText(f"{addr} → {value:.4f}")
|
||||
send_osc_message(addr, value)
|
||||
f._send_hw_feedback(slider_value)
|
||||
if f.output_mode == "ext":
|
||||
f.cc_output_lbl.setText(f"Ext [{slider_value}]")
|
||||
else:
|
||||
f.cc_output_lbl.setText(f._osc_output_label(slider_value))
|
||||
f.readout.setText(f"[{value:.4f}]")
|
||||
f._emit_value(slider_value) # handles OSC vs Ext, and hw feedback
|
||||
if ws_server:
|
||||
ws_server.broadcast_widget_update_f(uid, value)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user