diff --git a/faderwidget.py b/faderwidget.py index 14f9e35..3abc882 100644 --- a/faderwidget.py +++ b/faderwidget.py @@ -22,10 +22,12 @@ from midi_sender import send_cc class FaderWidget(QWidget): def __init__(self, label, default_cc): + super().__init__() - self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) - self.setFixedWidth(100) - self.current_color = "transparent" + + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Maximum) + self.setFixedWidth(115) + self.current_color = PALETTE_HEX.get("Gray", "#263238") self.uid = str(uuid.uuid4()) @@ -48,41 +50,96 @@ class FaderWidget(QWidget): inner.setSpacing(4) inner.setAlignment(Qt.AlignmentFlag.AlignHCenter) - # Learn button + # Learn / CC / CH group self.hw_cc = None + self.hw_channel = None self.is_learning = False + + _grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }" + + # Group 1: Learn + input readout (border always visible; learn_btn hideable) + self.grp_learn = QFrame() + self.grp_learn.setStyleSheet(_grp_style) + _grp1_layout = QVBoxLayout(self.grp_learn) + _grp1_layout.setContentsMargins(4, 4, 4, 4) + _grp1_layout.setSpacing(3) + self.learn_btn = QPushButton("Learn") self.learn_btn.setFixedHeight(20) + self.learn_btn.setStyleSheet("border: none;") self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu) - inner.addWidget(self.learn_btn) + _grp1_layout.addWidget(self.learn_btn) - # CC In label self.cc_input_lbl = QLabel("") self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;") - inner.addWidget(self.cc_input_lbl) + self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;") + self.cc_input_lbl.setFixedHeight(16) + _grp1_layout.addWidget(self.cc_input_lbl) + + inner.addWidget(self.grp_learn) + + _arrow_lbl = QLabel("▼") + _arrow_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) + _arrow_lbl.setStyleSheet("color: rgba(255,255,255,0.3); font-size: 9px; background: transparent;") + _arrow_lbl.setFixedHeight(12) + inner.addWidget(_arrow_lbl) + + # Group 2: CC spin + CH spin + output readout (border always visible; spins hideable) + self.grp_cc = QFrame() + self.grp_cc.setStyleSheet(_grp_style) + _grp2_layout = QVBoxLayout(self.grp_cc) + _grp2_layout.setContentsMargins(4, 4, 4, 4) + _grp2_layout.setSpacing(3) - # CC spin self.cc_spin = QSpinBox() - self.cc_spin.setMinimum(1) + self.cc_spin.setMinimum(0) self.cc_spin.setMaximum(127) self.cc_spin.setValue(default_cc) - self.cc_spin.setFixedWidth(75) - inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter) + self.cc_spin.setFixedWidth(55) + _cc_lbl = QLabel("CC") + _cc_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;") + self.cc_spin_row = QWidget() + _hbox_cc = QHBoxLayout(self.cc_spin_row) + _hbox_cc.setContentsMargins(0, 0, 0, 0) + _hbox_cc.setSpacing(4) + _hbox_cc.addWidget(_cc_lbl) + _hbox_cc.addWidget(self.cc_spin) + _grp2_layout.addWidget(self.cc_spin_row) - self.cc_output_lbl = QLabel("") + self.ch_spin = QSpinBox() + self.ch_spin.setMinimum(1) + self.ch_spin.setMaximum(16) + self.ch_spin.setValue(1) + self.ch_spin.setFixedWidth(55) + _ch_lbl = QLabel("CH") + _ch_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;") + self.ch_spin_row = QWidget() + _hbox_ch = QHBoxLayout(self.ch_spin_row) + _hbox_ch.setContentsMargins(0, 0, 0, 0) + _hbox_ch.setSpacing(4) + _hbox_ch.addWidget(_ch_lbl) + _hbox_ch.addWidget(self.ch_spin) + _grp2_layout.addWidget(self.ch_spin_row) + + self.cc_output_lbl = QLabel(f"CC{default_cc} [--]") self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") - inner.addWidget(self.cc_output_lbl) + self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;") + self.cc_output_lbl.setFixedHeight(16) + _grp2_layout.addWidget(self.cc_output_lbl) + + inner.addWidget(self.grp_cc) + + # Fader self.fader = QSlider(Qt.Orientation.Vertical) self.fader.setMinimum(0) self.fader.setMaximum(127) self.fader.setValue(64) - self.fader.setFixedHeight(200) + self.fader.setMinimumHeight(200) self.fader.setFixedWidth(40) + self.fader.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding) self.fader.setStyleSheet(FADER_STYLE) inner.addWidget(self.fader, alignment=Qt.AlignmentFlag.AlignHCenter) @@ -214,7 +271,7 @@ class FaderWidget(QWidget): def on_fader_moved(self, value): cc_num = self.cc_spin.value() - self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]") + self.cc_output_lbl.setText(self._cc_output_label(value)) if self.pickup_mode and self.hunting: crossed = ( (self.prev_position < self.last_sent <= value) or @@ -226,21 +283,21 @@ class FaderWidget(QWidget): self._stop_blink() self._set_readout_style("orange") self.last_sent = value - send_cc(cc_num, value) + send_cc(cc_num, value, channel=self.ch_spin.value()) else: self.readout.setText(f"CC {cc_num} → {value}") return else: self.last_sent = value self.prev_position = value - send_cc(cc_num, value) + send_cc(cc_num, value, channel=self.ch_spin.value()) self.readout.setText(f"CC {cc_num} → {value}") def on_cc_changed(self): cc_num = self.cc_spin.value() value = self.fader.value() self.readout.setText(f"CC {cc_num} → {value}") - self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]") + self.cc_output_lbl.setText(self._cc_output_label(value)) if self.pickup_mode: self.hunting = True self._start_blink() @@ -307,6 +364,7 @@ class FaderWidget(QWidget): action = menu.exec(self.learn_btn.mapToGlobal(pos)) if action == clear_action: self.hw_cc = None + self.hw_channel = None self.is_learning = False self.learn_btn.setText("Learn") self.learn_btn.setStyleSheet("") @@ -326,20 +384,32 @@ class FaderWidget(QWidget): self.learn_btn.setText("Listening...") self.learn_btn.setStyleSheet("color: orange; font-weight: bold;") + def _cc_input_label(self, vel="--"): + if self.hw_cc is None: + return "" + ch = f" CH{self.hw_channel}" if self.hw_channel is not None else "" + return f"CC{self.hw_cc}{ch} [{vel}]" + + def _cc_output_label(self, vel="--"): + out_cc = self.cc_spin.value() + out_ch = self.ch_spin.value() + return f"CC{out_cc} CH{out_ch} [{vel}]" + def stop_learn(self): self.is_learning = False self.learn_btn.setStyleSheet("") if self.hw_cc is not None: self.learn_btn.setText(f"HW: CC{self.hw_cc}") - self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]") + self.cc_input_lbl.setText(self._cc_input_label()) self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") else: self.learn_btn.setText("Learn") self.cc_input_lbl.setText("") self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;") - def bind_hw_cc(self, cc_number): + def bind_hw_cc(self, cc_number, channel=None): self.hw_cc = cc_number + self.hw_channel = channel self.stop_learn() def get_state(self): @@ -347,11 +417,13 @@ class FaderWidget(QWidget): "uid": self.uid, "label": self.title_edit.text(), "cc": self.cc_spin.value(), + "ch": self.ch_spin.value(), "value": self.fader.value(), "color": self.color_name, "pickup": self.pickup_checkbox.isChecked(), "last_sent": self.last_sent, "hw_cc": self.hw_cc, + "hw_channel": self.hw_channel, "zone": self.zone, "center_index": self.center_index, "zone_index": self.zone_index @@ -362,6 +434,7 @@ class FaderWidget(QWidget): self.uid = state["uid"] self.title_edit.setText(state.get("label", "")) self.cc_spin.setValue(state.get("cc", 1)) + self.ch_spin.setValue(state.get("ch", 1)) self.last_sent = state.get("last_sent", 64) self.fader.setValue(state.get("value", 64)) color_name = state.get("color", "Gray") @@ -379,9 +452,10 @@ class FaderWidget(QWidget): self.zone_index = state.get("zone_index", None) self.zone_btn.set_state(self.zone == "left", self.zone == "right") self.hw_cc = state.get("hw_cc", None) + self.hw_channel = state.get("hw_channel", None) if self.hw_cc is not None: self.learn_btn.setText(f"HW: CC{self.hw_cc}") - self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]") + self.cc_input_lbl.setText(self._cc_input_label()) self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") else: self.learn_btn.setText("Learn") diff --git a/main.py b/main.py index 120b5fa..ee76e5c 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import sys import json import os import uuid +import socket import rtmidi from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, @@ -73,71 +74,11 @@ def set_window(w): global _window _window = w - _osc_server = None + _osc_thread = None -def start_osc_server(port=9000): - global _osc_server, _osc_thread - stop_osc_server() - d = osc_dispatcher.Dispatcher() - - def emit_raw(addr, *args): - val_str = " ".join(str(a) for a in args) if args else "" - osc_receiver.raw_message_signal.emit(addr, val_str) - - def on_play(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.play_signal.emit(int(args[0])) - def on_record(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.record_signal.emit(int(args[0])) - def on_tempo(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.tempo_signal.emit(float(args[0])) - def on_beat(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.position_signal.emit(str(args[0])) - def on_track_name(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.track_name_signal.emit(str(args[0])) - def on_preset(addr, *args): - emit_raw(addr, *args) - if args: osc_receiver.preset_name_signal.emit(str(args[0])) - def on_default(addr, *args): - emit_raw(addr, *args) - - d.map("/play", on_play) - d.map("/record", on_record) - d.map("/tempo", on_tempo) - d.map("/beat/str", on_beat) - d.map("/track/*", on_track_name) - d.map("/fx/*", on_preset) - d.map("/fxname", on_preset) - d.set_default_handler(on_default) - - try: - _osc_server = osc_server.ThreadingOSCUDPServer(("0.0.0.0", port), d) - _osc_thread = threading.Thread(target=_osc_server.serve_forever, daemon=True) - _osc_thread.start() - print(f"[OSC] Listening on port {port}") - return True - except Exception as e: - print(f"[OSC] Failed to start: {e}") - _osc_server = None - return False - -def stop_osc_server(): - global _osc_server, _osc_thread - if _osc_server: - _osc_server.shutdown() - _osc_server = None - _osc_thread = None - -def set_app_window(window: QMainWindow): - global app_window - app_window = window @@ -151,6 +92,30 @@ spacing = 10 padding = 40 loaded_presets = load_presets_file() app_window = None +toggles = [] +transports = [] +toggle_strip_width = 100 +midi_in_port = None + +fader_show_hide_config = False +transport_show_hide_config = False +toggle_show_hide_config = False + +uuid_presets_matched = [] +uuid_presets_received_values = [] +uuid_presets_batch_timer = None +_track_uuid_check_pending = False +container_preset_browser = None +container_rows_and_widgets = None +view_uuid_error_overlay = None +label_overlay_message = None +placeholder_fader_empty = None +placeholder_toggle_empty = None + + + + + #MARK: CREATE APP app = QApplication(sys.argv) @@ -165,33 +130,62 @@ hstack_preset_row = QHBoxLayout() #MARK: PRESENT menuPresets = QComboBox() menuPresets.setMinimumWidth(160) -menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets)) +menuPresets.currentTextChanged.connect(lambda name: on_preset_selected(menuPresets, app_window, ready, loaded_presets, on_load=preset_load)) + + +#button new blank preset +buttonNew = QPushButton("New") +buttonNew.setFixedWidth(45) +buttonNew.clicked.connect(lambda: new_blank_preset()) #button preset save buttonSave = QPushButton("Save") buttonSave.setFixedWidth(50) -buttonSave.clicked.connect(lambda: save_preset(app_window, menuPresets, loaded_presets)) +buttonSave.clicked.connect(lambda: save_preset(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_build=preset_build_snapshot, on_save_as=lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot))) #button preset save as -> save_as_btn buttonSaveAs = QPushButton("Save As") buttonSaveAs.setFixedWidth(60) -buttonSaveAs.clicked.connect(lambda: save_preset_as(app_window, menuPresets, loaded_presets)) +buttonSaveAs.clicked.connect(lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot)) + + #button preset delete -> del_btn buttonRemove = QPushButton("Remove") buttonRemove.setFixedWidth(65) -buttonRemove.clicked.connect(lambda: delete_preset(app_window, menuPresets, loaded_presets)) +buttonRemove.clicked.connect(lambda: delete_preset(app_window, menuPresets, loaded_presets, on_refresh=on_preset_deleted)) + +buttonExport = QPushButton("Export") +buttonExport.setFixedWidth(55) +buttonExport.clicked.connect(lambda: export_preset()) + +buttonImport = QPushButton("Import") +buttonImport.setFixedWidth(55) +buttonImport.clicked.connect(lambda: import_preset()) #save status label (should be used for other stuff as well) -> save_status_label labelStatus = QLabel("") labelStatus.setStyleSheet("color: #00e676; font-style: italic;") +# Widget: label_preset_uuid +label_preset_uuid = QLabel("") +label_preset_uuid.setFixedWidth(240) +label_preset_uuid.setStyleSheet("color: #888; font-size: 11px; font-family: monospace;") + +# Widget: button_preset_uuid_gen +button_preset_uuid_gen = QPushButton("Gen") +button_preset_uuid_gen.setFixedWidth(36) +button_preset_uuid_gen.clicked.connect(lambda: generate_preset_uuid()) + +# Widget: button_preset_uuid_copy +button_preset_uuid_copy = QPushButton("Copy") +button_preset_uuid_copy.setFixedWidth(40) +button_preset_uuid_copy.clicked.connect(lambda: copy_preset_uuid()) + #container io row hstack_io_row = QHBoxLayout() hstack_io_row.setSpacing(16) - - # REFACTOR MAP: Old names → New names # self.in_blink → activity_indicator_midi_in # self.in_status → label_connection_midi_in @@ -207,20 +201,20 @@ hstack_midi_in_activity_and_label = QHBoxLayout() # activity_indicator_midi_in activity_indicator_midi_in = QLabel("●") -activity_indicator_midi_in.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") +# activity_indicator_midi_in.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_midi_in label_connection_midi_in = QLabel("Not Connected") -label_connection_midi_in.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") +# label_connection_midi_in.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_midi_in -label_midi_in = QLabel("MIDI Input") -label_midi_in.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") +label_midi_in = QLabel("MIDI In") +# label_midi_in.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_midi_in picker_midi_in = QComboBox() picker_midi_in.setFixedWidth(200) -picker_midi_in.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") +# picker_midi_in.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # REFACTOR MAP: Old names → New names @@ -237,20 +231,20 @@ hstack_midi_out_activity_and_label = QHBoxLayout() # activity_indicator_midi_out activity_indicator_midi_out = QLabel("●") -activity_indicator_midi_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") +# activity_indicator_midi_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_midi_out label_connection_midi_out = QLabel("Not Connected") -label_connection_midi_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") +# label_connection_midi_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_midi_out -label_midi_out = QLabel("MIDI Output") -label_midi_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") +label_midi_out = QLabel("MIDI Out") +# label_midi_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_midi_out picker_midi_out = QComboBox() picker_midi_out.setFixedWidth(200) -picker_midi_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") +# picker_midi_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # REFACTOR MAP: Old names → New names # self.t_out_blink → activity_indicator_transport_out @@ -266,20 +260,20 @@ hstack_transport_out_activity_and_label = QHBoxLayout() # activity_indicator_transport_out activity_indicator_transport_out = QLabel("●") -activity_indicator_transport_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") +# activity_indicator_transport_out.setStyleSheet("color: #444; font-size: 14px; background-color: rgba(255, 0, 0, 0.15); border: 1px solid red; padding: 2px;") # label_connection_transport_out label_connection_transport_out = QLabel("Not Connected") -label_connection_transport_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") +# label_connection_transport_out.setStyleSheet("color: #888; background-color: rgba(0, 0, 255, 0.15); border: 1px solid blue; padding: 2px;") # label_transport_out -label_transport_out = QLabel("Transport Output") -label_transport_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") +label_transport_out = QLabel("MIDI Transport Out") +# label_transport_out.setStyleSheet("background-color: rgba(255, 255, 0, 0.15); border: 1px solid yellow; padding: 2px;") # picker_transport_out picker_transport_out = QComboBox() picker_transport_out.setFixedWidth(200) -picker_transport_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") +# picker_transport_out.setStyleSheet("background-color: rgba(0, 255, 0, 0.15); border: 1px solid green;") # REFACTOR MAP: Old names → New names # self.osc_blink → activity_indicator_osc @@ -343,6 +337,14 @@ button_osc_messages_log.setFixedWidth(80) button_osc_messages_log.setCheckable(True) +# Layout: hstack_osc_status +hstack_osc_status = QHBoxLayout() +hstack_osc_status.setSpacing(8) + +# Widget: label_osc_status +label_osc_status = QLabel("") +label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;") + # MARK: OSC STATUS ROW MODULE # REFACTOR MAP: Old names → New names # self.osc_panel → container_osc_feedback @@ -378,14 +380,14 @@ label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: b label_osc_record_state = QLabel("● REC") label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") -# Widget: label_osc_tempo -label_osc_tempo = QLabel("BPM: —") -label_osc_tempo.setStyleSheet("color: #888; font-size: 11px;") - # Widget: label_osc_position -label_osc_position = QLabel("Pos: —") +label_osc_position = QLabel("Time: —") label_osc_position.setStyleSheet("color: #888; font-size: 11px;") +# Widget: label_osc_bar +label_osc_bar = QLabel("Bar: —") +label_osc_bar.setStyleSheet("color: #888; font-size: 11px;") + # Widget: divider_osc_feedback divider_osc_feedback = QWidget() divider_osc_feedback.setFixedHeight(1) @@ -416,7 +418,7 @@ spacer_osc_info = QWidget() spacer_osc_info.setFixedWidth(16) # Container: container_osc_in_log -container_osc_in_log = QWidget(None, Qt.WindowType.Tool) +container_osc_in_log = QWidget(None, Qt.WindowType.Window) container_osc_in_log.setWindowTitle("OSC In — Incoming") container_osc_in_log.setStyleSheet("background-color: #1a1a1a;") container_osc_in_log.setFixedWidth(360) @@ -451,7 +453,7 @@ textedit_osc_in_log.setStyleSheet(""" """) # Container: container_osc_out_log -container_osc_out_log = QWidget(None, Qt.WindowType.Tool) +container_osc_out_log = QWidget(None, Qt.WindowType.Window) container_osc_out_log.setWindowTitle("OSC Out — Outgoing") container_osc_out_log.setStyleSheet("background-color: #1a1a1a;") container_osc_out_log.setFixedWidth(360) @@ -562,6 +564,18 @@ vsep_transport_right.setVisible(False) hstack_transport_zone_right = QHBoxLayout() hstack_transport_zone_right.setSpacing(0) +# Widgets: transport strip (initial) +for _label, _cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]: + _t = TransportWidget(_label, _cc) + transports.append(_t) + hstack_transport_center.addWidget(_t) + _t.plus_btn.clicked.connect(lambda checked=False, t=_t: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t)) + _t.minus_btn.clicked.connect(lambda checked=False, t=_t: transport_remove(t)) + _t.learn_btn.clicked.connect(lambda checked=False, t=_t: check_and_start_midi_learn(t)) + _t.on_select_callback = lambda strip: check_and_update_selected_strip(strip) + _t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip) +del _t, _label, _cc + # MARK: SELECTED CHANNEL ROW MODULE # REFACTOR MAP: Old names → New names # self.selected_label → label_selected_channel @@ -570,8 +584,8 @@ hstack_transport_zone_right.setSpacing(0) hstack_selected_channel_and_save_status = QHBoxLayout() # Widget: label_selected_channel -label_selected_channel = QLabel("Selected: None") -label_selected_channel.setStyleSheet("color: #00ff88; font-weight: bold;") +label_selected_channel = QLabel("Software item selected: None") +label_selected_channel.setStyleSheet("color: #00ff88; font-weight: bold; border: 1px solid #00ff88; padding: 2px 8px;") label_selected_channel.setAlignment(Qt.AlignmentFlag.AlignCenter) # MARK: FADERS ROW MODULE @@ -652,7 +666,52 @@ separator_vertical_toggle_zone_right.setVisible(False) hstack_toggle_zone_right = QHBoxLayout() hstack_toggle_zone_right.setSpacing(0) +# Layout: hstack_row_fader_control_buttons +hstack_row_fader_control_buttons = QHBoxLayout() +hstack_row_fader_control_buttons.setSpacing(8) +# Widget: button_fader_show_hide_config +_show_hide_btn_style = "QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 12px; font-weight: normal; } QPushButton:hover { border-color: #aaa; color: #fff; }" + +button_fader_show_hide_config = QPushButton("Hide Parameters") +button_fader_show_hide_config.setFixedWidth(120) +button_fader_show_hide_config.setCheckable(True) +button_fader_show_hide_config.setFlat(True) +button_fader_show_hide_config.setStyleSheet(_show_hide_btn_style) + +button_fader_clear_all = QPushButton("Clear All") +button_fader_clear_all.setFixedWidth(70) +button_fader_clear_all.setFlat(True) +button_fader_clear_all.setStyleSheet(_show_hide_btn_style) + +# MARK: TRANSPORT ROW CONTROLS MODULE +# Layout: hstack_row_transport_control_buttons +hstack_row_transport_control_buttons = QHBoxLayout() +hstack_row_transport_control_buttons.setSpacing(8) + +# Widget: button_transport_show_hide_config +button_transport_show_hide_config = QPushButton("Hide Parameters") +button_transport_show_hide_config.setFixedWidth(120) +button_transport_show_hide_config.setCheckable(True) +button_transport_show_hide_config.setFlat(True) +button_transport_show_hide_config.setStyleSheet(_show_hide_btn_style) + +# MARK: TOGGLE ROW CONTROLS MODULE +# Layout: hstack_row_toggle_control_buttons +hstack_row_toggle_control_buttons = QHBoxLayout() +hstack_row_toggle_control_buttons.setSpacing(8) + +# Widget: button_toggle_show_hide_config +button_toggle_show_hide_config = QPushButton("Hide Parameters") +button_toggle_show_hide_config.setFixedWidth(120) +button_toggle_show_hide_config.setCheckable(True) +button_toggle_show_hide_config.setFlat(True) +button_toggle_show_hide_config.setStyleSheet(_show_hide_btn_style) + +button_toggle_clear_all = QPushButton("Clear All") +button_toggle_clear_all.setFixedWidth(70) +button_toggle_clear_all.setFlat(True) +button_toggle_clear_all.setStyleSheet(_show_hide_btn_style) #MARK: MAIN WINDOW START @@ -666,6 +725,8 @@ class MainWindow(QMainWindow): global buttonSave global buttonSaveAs global buttonRemove + global buttonExport + global buttonImport global labelStatus global hstack_io_row global available_ports_midi_in @@ -689,13 +750,21 @@ class MainWindow(QMainWindow): main_layout.addLayout(hstack_preset_row) hstack_preset_row.addWidget(menuPresets) #dropdown + hstack_preset_row.addWidget(buttonNew) hstack_preset_row.addWidget(buttonSave) #save button hstack_preset_row.addWidget(buttonSaveAs) #save as button hstack_preset_row.addWidget(buttonRemove) #remove button + hstack_preset_row.addWidget(buttonExport) + hstack_preset_row.addWidget(buttonImport) + hstack_preset_row.addWidget(label_preset_uuid) + hstack_preset_row.addWidget(button_preset_uuid_gen) + hstack_preset_row.addWidget(button_preset_uuid_copy) + hstack_preset_row.addWidget(labelStatus) hstack_preset_row.addStretch() + hstack_preset_row.addWidget(label_selected_channel) #section seperator - main_layout.addWidget(self._separator()) + main_layout.addWidget(make_separator()) #MARK: MIDI IN COLUMN 1 #build the hstack with activity indicator and connection state label @@ -713,6 +782,7 @@ class MainWindow(QMainWindow): # picker_midi_in population & signal available_ports_midi_in = get_available_input_ports() + picker_midi_in.addItem("Not Connected") for display_name, idx, available in available_ports_midi_in: picker_midi_in.addItem(display_name) if not available: @@ -720,7 +790,7 @@ class MainWindow(QMainWindow): picker_midi_in.model().item(item_index).setEnabled(False) picker_midi_in.model().item(item_index).setForeground(QColor('#888888')) - picker_midi_in.currentTextChanged.connect(self.on_input_port_select) + picker_midi_in.currentTextChanged.connect(check_midi_port_avail_and_assign) #MARK: MIDI OUT COLUMN 2 @@ -743,8 +813,9 @@ class MainWindow(QMainWindow): vstack_midi_out.addWidget(picker_midi_out) # port dropdown # populate the dropdown + picker_midi_out.addItem("Not Connected") picker_midi_out.addItems(available_ports) - picker_midi_out.currentTextChanged.connect(self.on_port_select) + picker_midi_out.currentTextChanged.connect(set_port_midi_output) # add the combined stack to the row hstack_io_row.addLayout(vstack_midi_out) @@ -769,8 +840,9 @@ class MainWindow(QMainWindow): vstack_transport_out.addWidget(picker_transport_out) # port dropdown # populate the dropdown + picker_transport_out.addItem("Not Connected") picker_transport_out.addItems(available_ports) - picker_transport_out.currentTextChanged.connect(self.on_transport_output_select) + picker_transport_out.currentTextChanged.connect(set_port_transport_output) # add the combined stack to the row hstack_io_row.addLayout(vstack_transport_out) @@ -820,10 +892,10 @@ class MainWindow(QMainWindow): vstack_osc.addLayout(hstack_osc_buttons) # control buttons # connect signals - textbox_osc_daw_ip.textChanged.connect(self.on_osc_ip_changed) - spinner_osc_send_port.valueChanged.connect(self.on_osc_send_port_changed) - button_osc_toggle.clicked.connect(self.toggle_osc) - button_osc_messages_log.clicked.connect(self.toggle_osc_log) + textbox_osc_daw_ip.textChanged.connect(update_osc_server_ip) + spinner_osc_send_port.valueChanged.connect(update_osc_server_port) + button_osc_toggle.clicked.connect(start_stop_osc_server) + button_osc_messages_log.clicked.connect(log_ui_show_hide) # add the combined stack to the row hstack_io_row.addLayout(vstack_osc) @@ -833,7 +905,7 @@ class MainWindow(QMainWindow): hstack_io_row.addStretch() main_layout.addLayout(hstack_io_row) - main_layout.addWidget(self._separator()) + main_layout.addWidget(make_separator()) # MARK: OSC STATUS ROW @@ -842,8 +914,8 @@ class MainWindow(QMainWindow): global hstack_osc_transport_state global label_osc_play_state global label_osc_record_state - global label_osc_tempo global label_osc_position + global label_osc_bar global divider_osc_feedback global hstack_osc_info global label_track_header @@ -869,8 +941,8 @@ class MainWindow(QMainWindow): hstack_osc_transport_state.addWidget(label_osc_play_state) hstack_osc_transport_state.addWidget(label_osc_record_state) hstack_osc_transport_state.addStretch() - hstack_osc_transport_state.addWidget(label_osc_tempo) hstack_osc_transport_state.addWidget(label_osc_position) + hstack_osc_transport_state.addWidget(label_osc_bar) # assemble track and preset info row hstack_osc_info.addWidget(label_track_header) @@ -880,10 +952,15 @@ class MainWindow(QMainWindow): hstack_osc_info.addWidget(label_osc_preset) hstack_osc_info.addStretch() + # assemble status row + hstack_osc_status.addWidget(label_osc_status) + hstack_osc_status.addStretch() + # assemble main feedback container vstack_osc_feedback.addLayout(hstack_osc_transport_state) # play, record, tempo, position vstack_osc_feedback.addWidget(divider_osc_feedback) # divider line vstack_osc_feedback.addLayout(hstack_osc_info) # track and preset labels + vstack_osc_feedback.addLayout(hstack_osc_status) # osc status messages container_osc_feedback.setVisible(False) main_layout.addWidget(container_osc_feedback) @@ -918,38 +995,19 @@ class MainWindow(QMainWindow): container_osc_out_log.setVisible(False) - #MARK: PRESET BROWSER - # Globals for mutation safety - global label_preset_browser_title - global hstack_preset_browser - global button_preset_browser_back - global spinner_preset_browser_back - global button_preset_browser_fwd - global spinner_preset_browser_fwd - global spacer_preset_browser + main_layout.addWidget(make_separator()) - # assemble browser controls - hstack_preset_browser.addWidget(button_preset_browser_back) - hstack_preset_browser.addWidget(spinner_preset_browser_back) - hstack_preset_browser.addWidget(spacer_preset_browser) - hstack_preset_browser.addWidget(button_preset_browser_fwd) - hstack_preset_browser.addWidget(spinner_preset_browser_fwd) - hstack_preset_browser.addStretch() + # MARK: TRANSPORT ROW CONTROLS + global hstack_row_transport_control_buttons + global button_transport_show_hide_config - # add to main layout - main_layout.addWidget(label_preset_browser_title) - main_layout.addLayout(hstack_preset_browser) - - # connect signals - button_preset_browser_back.clicked.connect(lambda: send_cc(spinner_preset_browser_back.value(), 127)) - button_preset_browser_fwd.clicked.connect(lambda: send_cc(spinner_preset_browser_fwd.value(), 127)) - - main_layout.addWidget(self._separator()) + hstack_row_transport_control_buttons.addWidget(button_transport_show_hide_config) + hstack_row_transport_control_buttons.addStretch() + button_transport_show_hide_config.clicked.connect(transport_show_hide_config_toggle) + main_layout.addLayout(hstack_row_transport_control_buttons) #MARK: TRANSPORT ROW - # Instance state - self.transports = [] - self.transport_strip_width = 100 + self._capturing_key_for = None # Globals for mutation safety @@ -960,32 +1018,67 @@ class MainWindow(QMainWindow): global vsep_transport_right global hstack_transport_zone_right - #MARK: TRANSPORT ROW # assemble full transport row - hstack_full_transport.addLayout(hstack_transport_zone_left) # left zone slot - hstack_full_transport.addWidget(vsep_transport_left) # left separator - hstack_full_transport.addLayout(hstack_transport_center) # center controls - hstack_full_transport.addWidget(vsep_transport_right) # right separator - hstack_full_transport.addLayout(hstack_transport_zone_right) # right zone slot + hstack_full_transport.addLayout(hstack_transport_zone_left) + hstack_full_transport.addWidget(vsep_transport_left) + hstack_full_transport.addLayout(hstack_transport_center) + hstack_full_transport.addWidget(vsep_transport_right) + hstack_full_transport.addLayout(hstack_transport_zone_right) - # add to main layout main_layout.addLayout(hstack_full_transport) - # MARK: SELECTED CHANNEL ROW - global hstack_selected_channel_and_save_status - global label_selected_channel - global labelStatus + #MARK: PRESET BROWSER CONTAINER + global container_preset_browser + global label_preset_browser_title + global hstack_preset_browser + global button_preset_browser_back + global spinner_preset_browser_back + global button_preset_browser_fwd + global spinner_preset_browser_fwd + global spacer_preset_browser - #MARK: SELECTED CHANNEL AND SAVE STATUS ROW - # assemble info row - hstack_selected_channel_and_save_status.addStretch() - hstack_selected_channel_and_save_status.addWidget(label_selected_channel) - hstack_selected_channel_and_save_status.addStretch() - labelStatus.setParent(None) # reparent status label - hstack_selected_channel_and_save_status.addWidget(labelStatus) + container_preset_browser = QWidget() + vstack_preset_browser_container = QVBoxLayout(container_preset_browser) + vstack_preset_browser_container.setContentsMargins(0, 0, 0, 0) - # add to main layout - main_layout.addLayout(hstack_selected_channel_and_save_status) + # assemble browser controls + hstack_preset_browser.addWidget(button_preset_browser_back) + hstack_preset_browser.addWidget(spinner_preset_browser_back) + hstack_preset_browser.addWidget(spacer_preset_browser) + hstack_preset_browser.addWidget(button_preset_browser_fwd) + hstack_preset_browser.addWidget(spinner_preset_browser_fwd) + hstack_preset_browser.addStretch(1) + + vstack_preset_browser_container.addWidget(make_separator()) + vstack_preset_browser_container.addWidget(label_preset_browser_title) + vstack_preset_browser_container.addLayout(hstack_preset_browser) + main_layout.addWidget(container_preset_browser) + + # connect signals + button_preset_browser_back.clicked.connect(lambda: send_cc(spinner_preset_browser_back.value(), 127)) + button_preset_browser_fwd.clicked.connect(lambda: send_cc(spinner_preset_browser_fwd.value(), 127)) + + # MARK: CONTAINER ROWS AND WIDGETS (fader + toggle only) + global container_rows_and_widgets + global view_uuid_error_overlay + global label_overlay_message + container_rows_and_widgets = QWidget() + controls_layout = QVBoxLayout(container_rows_and_widgets) + controls_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(container_rows_and_widgets) + + controls_layout.addWidget(make_separator()) + + # MARK: FADER ROW CONTROLS + global hstack_row_fader_control_buttons + global button_fader_show_hide_config + + hstack_row_fader_control_buttons.addWidget(button_fader_show_hide_config) + hstack_row_fader_control_buttons.addWidget(button_fader_clear_all) + hstack_row_fader_control_buttons.addStretch() + button_fader_show_hide_config.clicked.connect(fader_show_hide_config_toggle) + button_fader_clear_all.clicked.connect(clear_all_faders) + controls_layout.addLayout(hstack_row_fader_control_buttons) #MARK: FADERS ROW global hstack_full_fader @@ -996,19 +1089,31 @@ class MainWindow(QMainWindow): global hstack_fader_zone_right # assemble full fader row - hstack_full_fader.addLayout(hstack_fader_zone_left) # left zone slot - hstack_full_fader.addWidget(separator_vertical_fader_zone_left) # left separator - hstack_full_fader.addLayout(hstack_fader_center) # center faders - hstack_full_fader.addWidget(separator_vertical_fader_zone_right) # right separator - hstack_full_fader.addLayout(hstack_fader_zone_right) # right zone slot + hstack_full_fader.addLayout(hstack_fader_zone_left) + hstack_full_fader.addWidget(separator_vertical_fader_zone_left) + hstack_full_fader.addLayout(hstack_fader_center) + hstack_full_fader.addWidget(separator_vertical_fader_zone_right) + hstack_full_fader.addLayout(hstack_fader_zone_right) - # add to main layout - main_layout.addLayout(hstack_full_fader) - main_layout.addWidget(self._separator()) + controls_layout.addLayout(hstack_full_fader) - #MARK: TOGGLE ROW - self.toggles = [] - self.toggle_strip_width = 100 + global placeholder_fader_empty + placeholder_fader_empty = QWidget() + _pf_layout = QVBoxLayout(placeholder_fader_empty) + _pf_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + _pf_lbl = QLabel("No fader strips yet") + _pf_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + _pf_lbl.setStyleSheet("color: #666; font-size: 13px;") + _pf_btn = QPushButton("+ Add Fader") + _pf_btn.setFixedWidth(160) + _pf_btn.setFixedHeight(48) + _pf_btn.setStyleSheet("QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 13px; } QPushButton:hover { border-color: #aaa; color: #fff; }") + _pf_btn.clicked.connect(fader_add) + _pf_layout.addWidget(_pf_btn, alignment=Qt.AlignmentFlag.AlignCenter) + _pf_layout.addWidget(_pf_lbl) + controls_layout.addWidget(placeholder_fader_empty) + + controls_layout.addWidget(make_separator()) # Globals for mutation safety global hstack_full_toggle @@ -1018,16 +1123,54 @@ class MainWindow(QMainWindow): global separator_vertical_toggle_zone_right global hstack_toggle_zone_right + # MARK: TOGGLE ROW CONTROLS + global hstack_row_toggle_control_buttons + global button_toggle_show_hide_config + + hstack_row_toggle_control_buttons.addWidget(button_toggle_show_hide_config) + hstack_row_toggle_control_buttons.addWidget(button_toggle_clear_all) + hstack_row_toggle_control_buttons.addStretch() + button_toggle_show_hide_config.clicked.connect(toggle_show_hide_config_toggle) + button_toggle_clear_all.clicked.connect(clear_all_toggles) + controls_layout.addLayout(hstack_row_toggle_control_buttons) + #MARK: TOGGLES ROW # assemble full toggle row - hstack_full_toggle.addLayout(hstack_toggle_zone_left) # left zone slot - hstack_full_toggle.addWidget(separator_vertical_toggle_zone_left) # left separator - hstack_full_toggle.addLayout(hstack_toggle_center) # center toggles - hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right) # right separator - hstack_full_toggle.addLayout(hstack_toggle_zone_right) # right zone slot + hstack_full_toggle.addLayout(hstack_toggle_zone_left) + hstack_full_toggle.addWidget(separator_vertical_toggle_zone_left) + hstack_full_toggle.addLayout(hstack_toggle_center) + hstack_full_toggle.addWidget(separator_vertical_toggle_zone_right) + hstack_full_toggle.addLayout(hstack_toggle_zone_right) - # add to main layout - main_layout.addLayout(hstack_full_toggle) + controls_layout.addLayout(hstack_full_toggle) + + global placeholder_toggle_empty + placeholder_toggle_empty = QWidget() + _pt_layout = QVBoxLayout(placeholder_toggle_empty) + _pt_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + _pt_lbl = QLabel("No toggle strips yet") + _pt_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + _pt_lbl.setStyleSheet("color: #666; font-size: 13px;") + _pt_btn = QPushButton("+ Add Toggle") + _pt_btn.setFixedWidth(160) + _pt_btn.setFixedHeight(48) + _pt_btn.setStyleSheet("QPushButton { background: transparent; border: 1px solid #555; color: #aaa; border-radius: 4px; font-size: 13px; } QPushButton:hover { border-color: #aaa; color: #fff; }") + _pt_btn.clicked.connect(toggle_add) + _pt_layout.addWidget(_pt_btn, alignment=Qt.AlignmentFlag.AlignCenter) + _pt_layout.addWidget(_pt_lbl) + controls_layout.addWidget(placeholder_toggle_empty) + + controls_layout.addStretch() + + # MARK: UUID ERROR OVERLAY + view_uuid_error_overlay = QWidget() + vstack_overlay = QVBoxLayout(view_uuid_error_overlay) + vstack_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) + label_overlay_message = QLabel("") + label_overlay_message.setAlignment(Qt.AlignmentFlag.AlignCenter) + vstack_overlay.addWidget(label_overlay_message) + view_uuid_error_overlay.setVisible(False) + main_layout.addWidget(view_uuid_error_overlay) main_layout.addStretch() #MARK: SET DEFAULTS / LOAD PRESETS @@ -1035,820 +1178,1160 @@ class MainWindow(QMainWindow): loaded_presets = ensure_preset_structure(loaded_presets) - self.add_fader() - self.add_fader() - self.add_toggle() - for name, cc in [("Play", 117), ("Stop", 116), ("Record", 118), ("Arm", 119)]: - self.add_transport(name, cc) + # Migration: seed transport_preset from last used preset if not yet set + if not loaded_presets.get("transport_preset"): + last = loaded_presets.get("__last_used__") + if last and last in loaded_presets["presets"]: + seed = loaded_presets["presets"][last].get("transports", []) + if seed: + loaded_presets["transport_preset"] = seed + save_presets_file(loaded_presets) global ready ready = True - self.refresh_preset_combo() - self.restore_last_used() - self.refresh_all_id_labels() - self.refresh_port_availability() + populate_preset_dropdown_menu() + check_preset_previous_and_assign() + load_transport_preset() + load_io_config() + transport_show_hide_config_set(loaded_presets.get("transport_show_hide", False)) + check_and_index_strip_labels() + check_midi_port_availability() - midi_receiver.midi_in_signal.connect(lambda msg: self.blink(activity_indicator_midi_in)) - midi_receiver.midi_out_signal.connect(lambda: self.blink(activity_indicator_midi_out)) - midi_receiver.transport_out_signal.connect(lambda: self.blink(activity_indicator_transport_out)) - midi_receiver.hw_cc_signal.connect(self.on_hw_cc) + midi_receiver.midi_in_signal.connect(lambda msg: make_activity_blink(activity_indicator_midi_in)) + midi_receiver.midi_out_signal.connect(lambda: make_activity_blink(activity_indicator_midi_out)) + midi_receiver.transport_out_signal.connect(lambda: make_activity_blink(activity_indicator_transport_out)) + midi_receiver.hw_cc_signal.connect(midi_message_route_hw_to_output_cc) # OSC signals - osc_receiver.track_name_signal.connect(self.on_osc_track) - osc_receiver.preset_name_signal.connect(self.on_osc_preset) - osc_receiver.tempo_signal.connect(self.on_osc_tempo) - osc_receiver.position_signal.connect(self.on_osc_position) - osc_receiver.play_signal.connect(self.on_osc_play) - osc_receiver.record_signal.connect(self.on_osc_record) - osc_receiver.raw_message_signal.connect(self.on_osc_raw) - osc_receiver.osc_out_signal.connect(self.on_osc_out_raw) - - - - def refresh_all_id_labels(self): - # Center faders - - global faders - - center = [f for f in faders if not f.zone] - for i, f in enumerate(center): - f.update_id_label(None, i) - # Left zone faders - left = sorted([f for f in faders if f.zone == "left"], key=lambda f: f.zone_index or 0) - for i, f in enumerate(left): - f.update_id_label("left", i) - # Right zone faders - right = sorted([f for f in faders if f.zone == "right"], key=lambda f: f.zone_index or 0) - for i, f in enumerate(right): - f.update_id_label("right", i) - # Center toggles - t_center = [t for t in self.toggles if not t.zone] - for i, t in enumerate(t_center): - t.update_id_label(None, i) - # Left zone toggles - t_left = sorted([t for t in self.toggles if t.zone == "left"], key=lambda t: t.zone_index or 0) - for i, t in enumerate(t_left): - t.update_id_label("left", i) - # Right zone toggles - t_right = sorted([t for t in self.toggles if t.zone == "right"], key=lambda t: t.zone_index or 0) - for i, t in enumerate(t_right): - t.update_id_label("right", i) - # Transports - tr_center = [t for t in self.transports if not t.zone] - for i, t in enumerate(tr_center): - t.update_id_label(None, i) - tr_left = sorted([t for t in self.transports if t.zone == "left"], key=lambda t: t.zone_index or 0) - for i, t in enumerate(tr_left): - t.update_id_label("left", i) - tr_right = sorted([t for t in self.transports if t.zone == "right"], key=lambda t: t.zone_index or 0) - for i, t in enumerate(tr_right): - t.update_id_label("right", i) - - def on_toggle_zone_change(self, toggle): - if toggle.zone is not None and toggle.center_index is None: - if toggle in self.toggles: - toggle.center_index = self.toggles.index(toggle) - - for layout in [hstack_toggle_center, hstack_toggle_zone_left, hstack_toggle_zone_right]: - for i in range(layout.count()): - item = layout.itemAt(i) - if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == toggle.uid: - layout.removeWidget(toggle) - toggle.setParent(None) - break - - if toggle.zone == "left": - toggle.zone_index = hstack_toggle_zone_left.count() - 1 - hstack_toggle_zone_left.addWidget(toggle) - separator_vertical_toggle_zone_left.setVisible(True) - - elif toggle.zone == "right": - toggle.zone_index = hstack_toggle_zone_right.count() - 1 - hstack_toggle_zone_right.addWidget(toggle) - separator_vertical_toggle_zone_right.setVisible(True) - - else: - idx = toggle.center_index if toggle.center_index is not None else hstack_toggle_center.count() - idx = min(idx, hstack_toggle_center.count()) - hstack_toggle_center.insertWidget(idx, toggle) - toggle.center_index = None - toggle.zone_index = None - separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in self.toggles)) - separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in self.toggles)) - has_left = any(t.zone == "left" for t in self.toggles) - has_right = any(t.zone == "right" for t in self.toggles) - - self.update_window_width() - self.refresh_all_id_labels() - - def on_fader_zone_change(self, fader): - # Save center index before removing from center row - - global faders - - if fader.zone is not None and fader.center_index is None: - # Being zoned for first time — save current center position - if fader in faders: - fader.center_index = faders.index(fader) - - # Remove by UUID from whichever slot it's in - for layout in [hstack_fader_center, hstack_fader_zone_left, hstack_fader_zone_right]: - for i in range(layout.count()): - item = layout.itemAt(i) - if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == fader.uid: - layout.removeWidget(fader) - fader.setParent(None) - break - - if fader.zone == "left": - fader.zone_index = hstack_fader_zone_left.count() - 1 # subtract placeholder - hstack_fader_zone_left.addWidget(fader) - separator_vertical_fader_zone_left.setVisible(True) - - elif fader.zone == "right": - fader.zone_index = hstack_fader_zone_right.count() - 1 - hstack_fader_zone_right.addWidget(fader) - separator_vertical_fader_zone_right.setVisible(True) - - else: - idx = fader.center_index if fader.center_index is not None else hstack_fader_center.count() - idx = min(idx, hstack_fader_center.count()) - hstack_fader_center.insertWidget(idx, fader) - fader.center_index = None - fader.zone_index = None - separator_vertical_fader_zone_left.setVisible(any(f.zone == "left" for f in faders)) - separator_vertical_fader_zone_right.setVisible(any(f.zone == "right" for f in faders)) - has_left = any(f.zone == "left" for f in faders) - has_right = any(f.zone == "right" for f in faders) - - self.update_window_width() - - self.refresh_all_id_labels() - - def select_strip(self, strip): - global selected_strip - if selected_strip and selected_strip != strip: - selected_strip.set_selected(False) - selected_strip = strip - strip.set_selected(True) - label_selected_channel.setText(f"Selected: {strip.title_edit.text()}") - - def add_toggle(self, insert_after=None): - n = len(self.toggles) + 1 - toggle = ToggleWidget(f"Toggle {n}", default_cc=20) - if insert_after is not None and insert_after in self.toggles: - idx = self.toggles.index(insert_after) - self.toggles.insert(idx + 1, toggle) - hstack_toggle_center.insertWidget(idx + 1, toggle) - else: - self.toggles.append(toggle) - hstack_toggle_center.addWidget(toggle) - toggle.plus_btn.clicked.connect(lambda: self.add_toggle(insert_after=toggle)) - toggle.minus_btn.clicked.connect(lambda: self.remove_toggle(toggle)) - toggle.on_select_callback = self.select_strip - toggle.on_zone_change_callback = self.on_toggle_zone_change - toggle.learn_btn.clicked.connect(lambda: self.start_learn(toggle)) - self.update_window_width() - self.refresh_all_id_labels() - - def remove_toggle(self, toggle): - global selected_strip - if len(self.toggles) <= 1: - return - if selected_strip == toggle: - selected_strip = None - label_selected_channel.setText("Selected: None") - hstack_toggle_center.removeWidget(toggle) - toggle.deleteLater() - self.toggles.remove(toggle) - self.update_window_width() - self.refresh_all_id_labels() - - def add_fader(self, insert_after=None): - - global faders - - n = len(faders) + 1 - fader = FaderWidget(f"Fader {n}", default_cc=1) - if insert_after is not None and insert_after in faders: - idx = faders.index(insert_after) - faders.insert(idx + 1, fader) - hstack_fader_center.insertWidget(idx + 1, fader) - else: - faders.append(fader) - hstack_fader_center.addWidget(fader) - fader.plus_btn.clicked.connect(lambda: self.add_fader(insert_after=fader)) - fader.minus_btn.clicked.connect(lambda: self.remove_fader(fader)) - fader.learn_btn.clicked.connect(lambda: self.start_learn(fader)) - fader.on_select_callback = self.select_strip - fader.on_zone_change_callback = self.on_fader_zone_change - self.update_window_width() - self.refresh_all_id_labels() - - def remove_fader(self, fader): - - global selected_strip - global faders - global spacing - - if len(faders) <= 1: - return - if selected_strip == fader: - selected_strip = None - label_selected_channel.setText("Selected: None") - hstack_fader_center.removeWidget(fader) - fader.deleteLater() - faders.remove(fader) - self.update_window_width() - self.refresh_all_id_labels() - - def update_window_width(self): - - global faders - global strip_width - - nf = len(faders) - nt = len(self.toggles) - fader_w = (nf * strip_width) + ((nf - 1) * spacing) + padding - toggle_w = (nt * self.toggle_strip_width) + ((nt - 1) * spacing) + padding - w = max(fader_w, toggle_w, 300) - self.resize(w, self.height()) - - def _separator(self): - sep = QWidget() - sep.setFixedHeight(1) - sep.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) - sep.setStyleSheet("background-color: #3a3a3a;") - return sep - - def refresh_preset_combo(self): - - global loaded_presets - global menuPresets - - menuPresets.blockSignals(True) - menuPresets.clear() - menuPresets.addItems(sorted(loaded_presets["presets"].keys())) - menuPresets.blockSignals(False) - - def restore_last_used(self): - - global loaded_presets - global menuPresets - - last = loaded_presets.get("__last_used__") - if last and last in loaded_presets["presets"]: - menuPresets.blockSignals(True) - menuPresets.setCurrentText(last) - menuPresets.blockSignals(False) - self.apply_preset(loaded_presets["presets"][last]) - print(f"Restored last used preset: {last}") - - - - def _show_save_status(self): - - global labelStatus - - from datetime import datetime, date - now = datetime.now() - today = date.today() - time_part = now.strftime('%I:%M:%S %p').lstrip('0') - if now.date() == today: - time_str = f"Today {time_part}" - elif (today - now.date()).days == 1: - time_str = f"Yesterday {time_part}" - else: - time_str = f"{now.strftime('%b')} {now.day} {time_part}" - labelStatus.setText(f"[ Saved {time_str} ]") - QTimer.singleShot(4000, lambda: labelStatus.setText("")) - - def _build_preset_data(self, name): - - global faders - - return { - "midi_port": picker_midi_out.currentText(), - "midi_input": picker_midi_in.currentText(), - "transport_port": picker_transport_out.currentText(), - "faders": [f.get_state() for f in faders], - "toggles": [t.get_state() for t in self.toggles], - "transports": [t.get_state() for t in self.transports], - "browser": { - "back_cc": spinner_preset_browser_back.value(), - "fwd_cc": spinner_preset_browser_fwd.value() - }, - "osc": { - "ip": textbox_osc_daw_ip.text(), - "listen_port": spinner_osc_listen_port.value(), - "send_port": spinner_osc_send_port.value(), - } - } - - - def apply_preset(self, preset): - - global faders - global available_ports_midi_in - - port_name = preset.get("midi_port", "") - if port_name in available_ports: - picker_midi_out.setCurrentText(port_name) - - transport_port = preset.get("transport_port", "") - if transport_port in available_ports: - picker_transport_out.setCurrentText(transport_port) - - midi_input = preset.get("midi_input", "") - input_names = [entry[0] for entry in available_ports_midi_in] - if midi_input in input_names: - picker_midi_in.setCurrentText(midi_input) - fader_states = preset.get("faders", []) - while len(faders) < len(fader_states): - self.add_fader() - while len(faders) > len(fader_states): - widget = faders.pop() - hstack_fader_center.removeWidget(widget) - widget.deleteLater() - self.update_window_width() - for i, widget in enumerate(faders): - widget.set_state(fader_states[i]) - - # First place all center faders in their saved positions - center_faders = sorted( - [f for f in faders if not f.zone], - key=lambda f: f.center_index if f.center_index is not None else 999 - ) - for f in center_faders: - hstack_fader_center.removeWidget(f) - f.setParent(None) - for f in center_faders: - hstack_fader_center.addWidget(f) - - # Then place zoned faders in zone order - left_faders = sorted( - [f for f in faders if f.zone == "left"], - key=lambda f: f.zone_index if f.zone_index is not None else 999 - ) - right_faders = sorted( - [f for f in faders if f.zone == "right"], - key=lambda f: f.zone_index if f.zone_index is not None else 999 - ) - for f in left_faders: - hstack_fader_center.removeWidget(f) - f.setParent(None) - hstack_fader_zone_left.addWidget(f) - for f in right_faders: - hstack_fader_center.removeWidget(f) - f.setParent(None) - hstack_fader_zone_right.addWidget(f) - - self.update_window_width() - # Migration: old presets have 'toggle' (singular), new have 'toggles' (list) - if "toggles" in preset: - toggle_states = preset["toggles"] - elif "toggle" in preset: - toggle_states = [preset["toggle"]] - else: - toggle_states = [] - - if not toggle_states: - if len(self.toggles) == 0: - self.add_toggle() - else: - while len(self.toggles) < len(toggle_states): - self.add_toggle() - while len(self.toggles) > len(toggle_states): - widget = self.toggles.pop() - hstack_toggle_center.removeWidget(widget) - widget.deleteLater() - self.update_window_width() - for i, widget in enumerate(self.toggles): - widget.set_state(toggle_states[i]) - - # Place center toggles - center_toggles = sorted( - [t for t in self.toggles if not t.zone], - key=lambda t: t.center_index if t.center_index is not None else 999 - ) - for t in center_toggles: - hstack_toggle_center.removeWidget(t) - t.setParent(None) - for t in center_toggles: - hstack_toggle_center.addWidget(t) - - # Place zoned toggles - left_toggles = sorted( - [t for t in self.toggles if t.zone == "left"], - key=lambda t: t.zone_index if t.zone_index is not None else 999 - ) - right_toggles = sorted( - [t for t in self.toggles if t.zone == "right"], - key=lambda t: t.zone_index if t.zone_index is not None else 999 - ) - for t in left_toggles: - hstack_toggle_center.removeWidget(t) - t.setParent(None) - hstack_toggle_zone_left.addWidget(t) - for t in right_toggles: - hstack_toggle_center.removeWidget(t) - t.setParent(None) - hstack_toggle_zone_right.addWidget(t) - - - browser = preset.get("browser", {}) - spinner_preset_browser_back.setValue(browser.get("back_cc", 22)) - spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23)) - - transport_states = preset.get("transports", []) - for i, t in enumerate(self.transports): - if i < len(transport_states): - t.set_state(transport_states[i]) - - osc = preset.get("osc", {}) - if osc: - textbox_osc_daw_ip.setText(osc.get("ip", "127.0.0.1")) - spinner_osc_listen_port.setValue(osc.get("listen_port", 9000)) - spinner_osc_send_port.setValue(osc.get("send_port", 8000)) - - - - - - - def on_input_port_select(self, display_name): - - global available_ports_midi_in - - if not display_name: - return - port_entry = next((entry for entry in available_ports_midi_in if entry[0] == display_name), None) - if port_entry is None: - return - display_name, original_index, available = port_entry - if not available: - label_connection_midi_in.setText("In use by another program") - label_connection_midi_in.setStyleSheet("color: #e67e22;") - return - if hasattr(self, "midi_in"): - close_midi_input_port(self.midi_in) - del self.midi_in - try: - - self.midi_in = open_midi_input_port(original_index, self.on_midi_in) - label_connection_midi_in.setText("Connected") - label_connection_midi_in.setStyleSheet("color: #27ae60;") - print(f"Opened input port: {display_name}") - except Exception as e: - print(f"Failed to open port {display_name}: {e}") - label_connection_midi_in.setText("Failed to open") - label_connection_midi_in.setStyleSheet("color: #c0392b;") - - def on_hw_cc(self, fader_uid, value): - - global faders - - for fader in faders: - if fader.uid == fader_uid: - out_cc = fader.cc_spin.value() - print(f"[convert] HW CC{fader.hw_cc} ({value}) → CC{out_cc} ({value})") - fader.fader.blockSignals(True) - fader.fader.setValue(value) - fader.fader.blockSignals(False) - fader.cc_input_lbl.setText(f"In CC{fader.hw_cc} [{value}]") - fader.cc_output_lbl.setText(f"Out CC{out_cc} [{value}]") - fader.readout.setText(f"CC {out_cc} → {value}") - fader.last_sent = value - fader.prev_position = value - send_cc(out_cc, value) - return - for t in self.transports: - if t.uid == fader_uid: - out_cc = t.cc_spin.value() - should_be_on = value > 63 - if t.toggle_state != should_be_on: - t.toggle_state = should_be_on - out_val = 127 if should_be_on else 0 - send_transport_cc(out_cc, out_val) - t.cc_input_lbl.setText(f"In CC{t.hw_cc} [{value}]") - t.cc_output_lbl.setText(f"Out CC{out_cc} [{out_val}]") - t._update_btn_style() - return - should_be_on = value > 63 - if toggle.toggle_state != should_be_on: - toggle.toggle_state = should_be_on - out_cc = toggle.cc_spin.value() - out_val = 127 if should_be_on else 0 - send_cc(out_cc, out_val) - toggle.cc_input_lbl.setText(f"In CC{toggle.hw_cc} [{value}]") - toggle.cc_output_lbl.setText(f"Out CC{out_cc} [{out_val}]") - toggle._update_btn_style() - return - - def start_learn(self, fader): - - global learning_strip - - if learning_strip and learning_strip != fader: - learning_strip.stop_learn() - learning_strip = fader - fader.start_learn() - - def on_midi_in(self, event, data=None): - - global learning_strip - global faders - - message, _ = event - status = message[0] & 0xF0 - cc_num = message[1] if len(message) >= 2 else None - cc_val = message[2] if len(message) >= 3 else None - - # Check if this CC is bound to a fader, toggle, or transport - bound_fader = None - bound_transport = None - if status == 0xB0: - for fader in faders: - if fader.hw_cc == cc_num: - bound_fader = fader - break - if not bound_fader: - for toggle in self.toggles: - if hasattr(toggle, 'hw_cc') and toggle.hw_cc == cc_num: - bound_fader = toggle - break - if not bound_fader: - for t in self.transports: - if hasattr(t, 'hw_cc') and t.hw_cc == cc_num: - bound_transport = t - break - - # Learning mode — bind CC to strip, don't forward - if status == 0xB0 and learning_strip: - learning_strip.bind_hw_cc(cc_num) - learning_strip = None - return - - if bound_transport: - # Transport CC — only goes to transport_midi_out - print(f"[transport hw] CC{cc_num} val={cc_val} → uid={bound_transport.uid}") - midi_receiver.hw_cc_signal.emit(bound_transport.uid, cc_val) - elif bound_fader: - # Regular fader/toggle — goes to midi_out - print(f"[hw] CC{cc_num} val={cc_val} → uid={bound_fader.uid} out_cc={bound_fader.cc_spin.value()}") - midi_receiver.hw_cc_signal.emit(bound_fader.uid, cc_val) - else: - # Not bound — pass through normally - print(f"[passthrough] CC{cc_num} val={cc_val}") - if midi_out.is_port_open(): - midi_out.send_message(message) - - midi_receiver.midi_in_signal.emit(list(message)) - - def blink(self, led): - led.setStyleSheet("color: #2ecc71; font-size: 14px;") - QTimer.singleShot(80, lambda: led.setStyleSheet("color: #444; font-size: 14px;")) - - def refresh_port_availability(self): - """Gray out ports already in use by another dropdown.""" - out_port = picker_midi_out.currentText() - t_port = picker_transport_out.currentText() - - # Output combo — gray out transport port - for i in range(picker_midi_out.model().rowCount()): - item = picker_midi_out.model().item(i) - # Strip any existing [in use] tag before comparing - base = item.text().replace(" [in use]", "") - if base == t_port and base != out_port: - item.setText(f"{base} [in use]") - item.setEnabled(False) - item.setForeground(QColor('#888888')) - else: - item.setText(base) - item.setEnabled(True) - item.setForeground(QColor('#ffffff')) - - # Transport combo — gray out output port - for i in range(picker_transport_out.model().rowCount()): - item = picker_transport_out.model().item(i) - base = item.text().replace(" [in use]", "") - if base == out_port and base != t_port: - item.setText(f"{base} [in use]") - item.setEnabled(False) - item.setForeground(QColor('#888888')) - else: - item.setText(base) - item.setEnabled(True) - item.setForeground(QColor('#ffffff')) - - def on_port_select(self, port_name): - if midi_out.is_port_open(): - midi_out.close_port() - port_index = available_ports.index(port_name) - midi_out.open_port(port_index) - label_connection_midi_out.setText("Connected") - label_connection_midi_out.setStyleSheet("color: #27ae60;") - print(f"Opened output port: {port_name}") - self.refresh_port_availability() - - def on_transport_output_select(self, port_name): - if not port_name: - return - if transport_midi_out.is_port_open(): - transport_midi_out.close_port() - port_index = available_ports.index(port_name) - transport_midi_out.open_port(port_index) - label_connection_transport_out.setText("Connected") - label_connection_transport_out.setStyleSheet("color: #27ae60;") - print(f"Opened transport output port: {port_name}") - self.refresh_port_availability() - - def add_transport(self, label, default_cc): - t = TransportWidget(label, default_cc) - t.on_select_callback = self.select_strip - t.learn_btn.clicked.connect(lambda: self.start_learn(t)) - t.on_zone_change_callback = self.on_transport_zone_change - self.transports.append(t) - hstack_transport_center.addWidget(t) - self.refresh_all_id_labels() - - def on_transport_zone_change(self, transport): - if transport.zone is not None and transport.center_index is None: - if transport in self.transports: - transport.center_index = self.transports.index(transport) - for layout in [hstack_transport_center, hstack_transport_zone_left, hstack_transport_zone_right]: - for i in range(layout.count()): - item = layout.itemAt(i) - if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == transport.uid: - layout.removeWidget(transport) - transport.setParent(None) - break - if transport.zone == "left": - transport.zone_index = hstack_transport_zone_left.count() - hstack_transport_zone_left.addWidget(transport) - vsep_transport_left.setVisible(True) - elif transport.zone == "right": - transport.zone_index = hstack_transport_zone_right.count() - hstack_transport_zone_right.addWidget(transport) - vsep_transport_right.setVisible(True) - else: - idx = transport.center_index if transport.center_index is not None else hstack_transport_center.count() - hstack_transport_center.insertWidget(min(idx, hstack_transport_center.count()), transport) - transport.center_index = None - transport.zone_index = None - vsep_transport_left.setVisible(any(t.zone == "left" for t in self.transports)) - vsep_transport_right.setVisible(any(t.zone == "right" for t in self.transports)) - self.refresh_all_id_labels() - - def toggle_osc_log(self, checked): - container_osc_in_log.setVisible(checked) - container_osc_out_log.setVisible(checked) - self._position_osc_log() - - def _position_osc_log(self): - geo = self.frameGeometry() - h = geo.height() - # In panel to the right - container_osc_in_log.setFixedHeight(h) - container_osc_in_log.move(geo.right() + 4, geo.top()) - # Out panel stacked below in panel - container_osc_out_log.setFixedWidth(360) - container_osc_out_log.setFixedHeight(h // 2) - container_osc_out_log.move(geo.right() + 4, geo.top() + h // 2 + 4) - # Adjust in panel height - container_osc_in_log.setFixedHeight(h // 2 - 4) + osc_receiver.track_name_signal.connect(update_ui_on_osc_track_received) + osc_receiver.preset_name_signal.connect(update_ui_on_osc_fx_info_received) + osc_receiver.bar_signal.connect(update_ui_on_osc_bar_received) + osc_receiver.position_signal.connect(update_ui_on_osc_playhead_received) + osc_receiver.play_signal.connect(update_ui_on_osc_play_transport_received) + osc_receiver.record_signal.connect(update_ui_on_osc_record_transport_received) + osc_receiver.raw_message_signal.connect(log_osc_incoming) + osc_receiver.osc_out_signal.connect(log_osc_outgoing) + osc_receiver.osc_out_signal.connect(lambda *_: make_activity_blink(activity_indicator_osc)) + osc_receiver.uuid_preset.connect(switch_preset_by_uuid) + + global uuid_presets_batch_timer + uuid_presets_batch_timer = QTimer() + uuid_presets_batch_timer.setSingleShot(True) + uuid_presets_batch_timer.timeout.connect(resolve_uuid_batch) + + QTimer.singleShot(0, start_stop_osc_server) def resizeEvent(self, event): super().resizeEvent(event) if container_osc_in_log.isVisible(): - self._position_osc_log() + log_ui_window_calc() def moveEvent(self, event): super().moveEvent(event) if container_osc_in_log.isVisible(): - self._position_osc_log() + log_ui_window_calc() - def on_osc_raw(self, addr, val): - from datetime import datetime - ts = datetime.now().strftime("%H:%M:%S") - line = f"{ts} {addr} {val}" - textedit_osc_in_log.append(line) - doc = textedit_osc_in_log.document() - if doc.blockCount() > 200: - cursor = textedit_osc_in_log.textCursor() - cursor.movePosition(cursor.MoveOperation.Start) - cursor.select(cursor.SelectionType.LineUnderCursor) - cursor.removeSelectedText() - cursor.deleteChar() +def make_separator(): + sep = QWidget() + sep.setFixedHeight(1) + sep.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + sep.setStyleSheet("background-color: #3a3a3a;") + return sep - def on_osc_out_raw(self, addr, val): - from datetime import datetime - ts = datetime.now().strftime("%H:%M:%S") - line = f"{ts} {addr} {val}" - textedit_osc_out_log.append(line) - doc = textedit_osc_out_log.document() - if doc.blockCount() > 200: - cursor = textedit_osc_out_log.textCursor() - cursor.movePosition(cursor.MoveOperation.Start) - cursor.select(cursor.SelectionType.LineUnderCursor) - cursor.removeSelectedText() - cursor.deleteChar() +def make_activity_blink(led): + led.setStyleSheet("color: #2ecc71; font-size: 14px;") + QTimer.singleShot(80, lambda: led.setStyleSheet("color: #444; font-size: 14px;")) - def on_osc_ip_changed(self, ip): - set_osc_target(ip=ip.strip() or "127.0.0.1") +def populate_preset_dropdown_menu(): - def on_osc_send_port_changed(self, port): - set_osc_target(port=port) + global loaded_presets - def toggle_osc(self): - if _osc_server is not None: - stop_osc_server() - label_connection_osc.setText("Not Listening") - label_connection_osc.setStyleSheet("color: #888;") - button_osc_toggle.setText("Start OSC") - activity_indicator_osc.setStyleSheet("color: #444; font-size: 14px;") - container_osc_feedback.setVisible(False) + global menuPresets + + menuPresets.blockSignals(True) + menuPresets.clear() + menuPresets.addItems(sorted(loaded_presets["presets"].keys())) + menuPresets.blockSignals(False) + +def clear_all_faders(): + for f in list(faders): + hstack_fader_center.removeWidget(f) + f.deleteLater() + faders.clear() + update_window_width() + _update_fader_placeholder() + +def clear_all_toggles(): + for t in list(toggles): + hstack_toggle_center.removeWidget(t) + t.deleteLater() + toggles.clear() + update_window_width() + _update_toggle_placeholder() + +def clear_all_strips(): + clear_all_faders() + clear_all_toggles() + +def on_preset_deleted(): + populate_preset_dropdown_menu() + if not loaded_presets.get("presets"): + clear_all_strips() + +def new_blank_preset(): + from PyQt6.QtWidgets import QInputDialog + name, ok = QInputDialog.getText(app_window, "New Preset", "Preset name:") + if not ok or not name.strip(): + return + name = name.strip() + loaded_presets["presets"][name] = {"faders": [], "toggles": [], "preset_uuid": "", "browser": {}, "show_hide": {}} + loaded_presets["__last_used__"] = name + save_presets_file(loaded_presets) + populate_preset_dropdown_menu() + menuPresets.setCurrentText(name) + clear_all_strips() + show_save_status() + +def show_save_status(): + + global labelStatus + + from datetime import datetime, date + now = datetime.now() + today = date.today() + time_part = now.strftime('%I:%M:%S %p').lstrip('0') + if now.date() == today: + time_str = f"Today {time_part}" + elif (today - now.date()).days == 1: + time_str = f"Yesterday {time_part}" + else: + time_str = f"{now.strftime('%b')} {now.day} {time_part}" + labelStatus.setText(f"[ Saved {time_str} ]") + QTimer.singleShot(4000, lambda: labelStatus.setText("")) + +def check_midi_port_availability(): + out_port = picker_midi_out.currentText() + t_port = picker_transport_out.currentText() + + for i in range(picker_midi_out.model().rowCount()): + item = picker_midi_out.model().item(i) + base = item.text().replace(" [in use]", "") + if base == t_port and base != out_port: + item.setText(f"{base} [in use]") + item.setEnabled(False) + item.setForeground(QColor('#888888')) else: - port = spinner_osc_listen_port.value() - ok = start_osc_server(port) - if ok: - label_connection_osc.setText(f"Listening :{port}") - label_connection_osc.setStyleSheet("color: #27ae60;") - button_osc_toggle.setText("Stop OSC") - activity_indicator_osc.setStyleSheet("color: #2ecc71; font-size: 14px;") - container_osc_feedback.setVisible(True) - else: - label_connection_osc.setText("Failed") - label_connection_osc.setStyleSheet("color: #c0392b;") + item.setText(base) + item.setEnabled(True) + item.setForeground(QColor('#ffffff')) - def on_osc_track(self, name): - label_osc_track.setText(name) - self.blink(activity_indicator_osc) - - def on_osc_preset(self, name): - label_osc_preset.setText(name) - self.blink(activity_indicator_osc) - - def on_osc_tempo(self, bpm): - label_osc_tempo.setText(f"BPM: {bpm:.1f}") - - def on_osc_position(self, pos): - label_osc_position.setText(f"Pos: {pos}") - - def on_osc_play(self, state): - if state: - label_osc_play_state.setText("▶ PLAYING") - label_osc_play_state.setStyleSheet("color: #00e676; font-size: 11px; font-weight: bold;") + for i in range(picker_transport_out.model().rowCount()): + item = picker_transport_out.model().item(i) + base = item.text().replace(" [in use]", "") + if base == out_port and base != t_port: + item.setText(f"{base} [in use]") + item.setEnabled(False) + item.setForeground(QColor('#888888')) else: - label_osc_play_state.setText("■ STOPPED") - label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") + item.setText(base) + item.setEnabled(True) + item.setForeground(QColor('#ffffff')) - def on_osc_record(self, state): - if state: - label_osc_record_state.setText("● REC") - label_osc_record_state.setStyleSheet("color: #e53935; font-size: 11px; font-weight: bold;") +def set_port_midi_output(port_name): + if midi_out.is_port_open(): + midi_out.close_port() + if port_name == "Not Connected" or port_name not in available_ports: + label_connection_midi_out.setText("Not Connected") + label_connection_midi_out.setStyleSheet("color: #888;") + save_io_config() + return + port_index = available_ports.index(port_name) + midi_out.open_port(port_index) + label_connection_midi_out.setText("Connected") + label_connection_midi_out.setStyleSheet("color: #27ae60;") + print(f"Opened output port: {port_name}") + check_midi_port_availability() + save_io_config() + + +def set_port_transport_output(port_name): + if transport_midi_out.is_port_open(): + transport_midi_out.close_port() + if port_name == "Not Connected" or not port_name or port_name not in available_ports: + label_connection_transport_out.setText("Not Connected") + label_connection_transport_out.setStyleSheet("color: #888;") + save_io_config() + return + port_index = available_ports.index(port_name) + transport_midi_out.open_port(port_index) + label_connection_transport_out.setText("Connected") + label_connection_transport_out.setStyleSheet("color: #27ae60;") + print(f"Opened transport output port: {port_name}") + check_midi_port_availability() + save_io_config() + +def log_osc_incoming(addr, val): + from datetime import datetime + ts = datetime.now().strftime("%H:%M:%S") + line = f"{ts} {addr} {val}" + textedit_osc_in_log.append(line) + doc = textedit_osc_in_log.document() + if doc.blockCount() > 200: + cursor = textedit_osc_in_log.textCursor() + cursor.movePosition(cursor.MoveOperation.Start) + cursor.select(cursor.SelectionType.LineUnderCursor) + cursor.removeSelectedText() + cursor.deleteChar() + +def log_osc_outgoing(addr, val): + + from datetime import datetime + + ts = datetime.now().strftime("%H:%M:%S") + + line = f"{ts} {addr} {val}" + textedit_osc_out_log.append(line) + doc = textedit_osc_out_log.document() + if doc.blockCount() > 200: + cursor = textedit_osc_out_log.textCursor() + cursor.movePosition(cursor.MoveOperation.Start) + cursor.select(cursor.SelectionType.LineUnderCursor) + cursor.removeSelectedText() + cursor.deleteChar() + +def update_osc_server_ip(ip): + set_osc_target(ip=ip.strip() or "127.0.0.1") + save_io_config() + +def update_osc_server_port(port): + set_osc_target(port=port) + save_io_config() + +def is_osc_port_in_use(port): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + try: + s.bind(("0.0.0.0", port)) + return False + except OSError: + return True + +def start_stop_osc_server(): + + if _osc_server is not None: + stop_osc_server() + label_connection_osc.setText("Not Listening") + label_connection_osc.setStyleSheet("color: #888;") + button_osc_toggle.setText("Start OSC") + activity_indicator_osc.setStyleSheet("color: #444; font-size: 14px;") + container_osc_feedback.setVisible(False) + else: + port = spinner_osc_listen_port.value() + if is_osc_port_in_use(port): + label_connection_osc.setText(f"Port {port} in use") + label_connection_osc.setStyleSheet("color: #e67e22;") + return + ok = start_osc_server(port) + if ok: + label_connection_osc.setText(f"Listening :{port}") + label_connection_osc.setStyleSheet("color: #27ae60;") + button_osc_toggle.setText("Stop OSC") + activity_indicator_osc.setStyleSheet("color: #2ecc71; font-size: 14px;") + container_osc_feedback.setVisible(True) else: - label_osc_record_state.setText("● REC") - label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") + label_connection_osc.setText("Failed") + label_connection_osc.setStyleSheet("color: #c0392b;") - def fire_transport(self, name): - for t in self.transports: - if t._label == name: - t.on_click() + +def _check_track_uuid_arrived(): + global _track_uuid_check_pending + if _track_uuid_check_pending: + _track_uuid_check_pending = False + label_osc_status.setText("No preset linked to this track") + label_osc_status.setStyleSheet("color: #f0c040; font-size: 11px; font-style: italic;") + ui_show_uuid_error_overlay("No preset linked to this track", "#f0c040") + +def update_ui_on_osc_track_received(name): + global _track_uuid_check_pending + label_osc_track.setText(name) + make_activity_blink(activity_indicator_osc) + label_osc_status.setText("") + _track_uuid_check_pending = True + QTimer.singleShot(400, _check_track_uuid_arrived) + +def update_ui_on_osc_fx_info_received(name): + label_osc_preset.setText(name) + make_activity_blink(activity_indicator_osc) + +def update_ui_on_osc_bar_received(bar): + label_osc_bar.setText(f"Bar: {bar}") + +def update_ui_on_osc_playhead_received(pos): + label_osc_position.setText(f"Time: {pos}") + +def update_ui_on_osc_play_transport_received(state): + if state: + label_osc_play_state.setText("▶ PLAYING") + label_osc_play_state.setStyleSheet("color: #00e676; font-size: 11px; font-weight: bold;") + else: + label_osc_play_state.setText("■ STOPPED") + label_osc_play_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") + +def update_ui_on_osc_record_transport_received(state): + if state: + label_osc_record_state.setText("● REC") + label_osc_record_state.setStyleSheet("color: #e53935; font-size: 11px; font-weight: bold;") + else: + label_osc_record_state.setText("● REC") + label_osc_record_state.setStyleSheet("color: #555; font-size: 11px; font-weight: bold;") + +def check_and_start_midi_learn(strip): + global learning_strip + + if learning_strip and learning_strip != strip: + learning_strip.stop_learn() + learning_strip = strip + strip.start_learn() + +def update_window_width(): + nf = len(faders) + nt = len(toggles) + ntr = len(transports) + fader_w = (nf * strip_width) + ((nf - 1) * spacing) + padding + toggle_w = (nt * toggle_strip_width) + ((nt - 1) * spacing) + padding + transport_w = (ntr * strip_width) + ((ntr - 1) * spacing) + padding + w = max(fader_w, toggle_w, transport_w, 300) + app_window.resize(w, app_window.height()) + +def check_and_index_strip_labels(): + + center = [f for f in faders if not f.zone] + + for i, f in enumerate(center): + f.update_id_label(None, i) + left = sorted([f for f in faders if f.zone == "left"], key=lambda f: f.zone_index or 0) + for i, f in enumerate(left): + f.update_id_label("left", i) + right = sorted([f for f in faders if f.zone == "right"], key=lambda f: f.zone_index or 0) + for i, f in enumerate(right): + f.update_id_label("right", i) + t_center = [t for t in toggles if not t.zone] + for i, t in enumerate(t_center): + t.update_id_label(None, i) + t_left = sorted([t for t in toggles if t.zone == "left"], key=lambda t: t.zone_index or 0) + for i, t in enumerate(t_left): + t.update_id_label("left", i) + t_right = sorted([t for t in toggles if t.zone == "right"], key=lambda t: t.zone_index or 0) + for i, t in enumerate(t_right): + t.update_id_label("right", i) + tr_center = [t for t in transports if not t.zone] + for i, t in enumerate(tr_center): + t.update_id_label(None, i) + tr_left = sorted([t for t in transports if t.zone == "left"], key=lambda t: t.zone_index or 0) + for i, t in enumerate(tr_left): + t.update_id_label("left", i) + tr_right = sorted([t for t in transports if t.zone == "right"], key=lambda t: t.zone_index or 0) + for i, t in enumerate(tr_right): + t.update_id_label("right", i) + + separator_vertical_fader_zone_left.setVisible(any(f.zone == "left" for f in faders)) + separator_vertical_fader_zone_right.setVisible(any(f.zone == "right" for f in faders)) + separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles)) + separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles)) + +def check_and_update_selected_strip(strip): + global selected_strip + if selected_strip and selected_strip != strip: + selected_strip.set_selected(False) + selected_strip = strip + strip.set_selected(True) + label_selected_channel.setText(f"Software item selected: {strip.title_edit.text()}") + +def handle_toggle_zone_change(toggle): + if toggle.zone is not None and toggle.center_index is None: + if toggle in toggles: + toggle.center_index = toggles.index(toggle) + + for layout in [hstack_toggle_center, hstack_toggle_zone_left, hstack_toggle_zone_right]: + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == toggle.uid: + layout.removeWidget(toggle) + toggle.setParent(None) break - def capture_key(self, name): - pass + if toggle.zone == "left": + toggle.zone_index = hstack_toggle_zone_left.count() - 1 + hstack_toggle_zone_left.addWidget(toggle) + separator_vertical_toggle_zone_left.setVisible(True) - def keyPressEvent(self, event): - super().keyPressEvent(event) + elif toggle.zone == "right": + toggle.zone_index = hstack_toggle_zone_right.count() - 1 + hstack_toggle_zone_right.addWidget(toggle) + separator_vertical_toggle_zone_right.setVisible(True) - def keyPressEvent(self, event): - if self._capturing_key_for: - name = self._capturing_key_for - entry = self.transport_buttons[name] - key = event.key() - modifiers = event.modifiers() - if modifiers == Qt.KeyboardModifier.ControlModifier: - entry["modifier"] = Qt.KeyboardModifier.ControlModifier - else: - entry["modifier"] = Qt.KeyboardModifier.NoModifier - entry["key"] = key - self._capturing_key_for = None + else: + idx = toggle.center_index if toggle.center_index is not None else hstack_toggle_center.count() + idx = min(idx, hstack_toggle_center.count()) + hstack_toggle_center.insertWidget(idx, toggle) + toggle.center_index = None + toggle.zone_index = None + separator_vertical_toggle_zone_left.setVisible(any(t.zone == "left" for t in toggles)) + separator_vertical_toggle_zone_right.setVisible(any(t.zone == "right" for t in toggles)) + + update_window_width() + check_and_index_strip_labels() + +def handle_fader_zone_change(fader): + if fader.zone is not None and fader.center_index is None: + if fader in faders: + fader.center_index = faders.index(fader) + + for layout in [hstack_fader_center, hstack_fader_zone_left, hstack_fader_zone_right]: + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == fader.uid: + layout.removeWidget(fader) + fader.setParent(None) + break + + if fader.zone == "left": + + fader.zone_index = hstack_fader_zone_left.count() - 1 + hstack_fader_zone_left.addWidget(fader) + separator_vertical_fader_zone_left.setVisible(True) + + elif fader.zone == "right": + + fader.zone_index = hstack_fader_zone_right.count() - 1 + hstack_fader_zone_right.addWidget(fader) + separator_vertical_fader_zone_right.setVisible(True) + + else: + idx = fader.center_index if fader.center_index is not None else hstack_fader_center.count() + idx = min(idx, hstack_fader_center.count()) + hstack_fader_center.insertWidget(idx, fader) + fader.center_index = None + fader.zone_index = None + separator_vertical_fader_zone_left.setVisible(any(f.zone == "left" for f in faders)) + separator_vertical_fader_zone_right.setVisible(any(f.zone == "right" for f in faders)) + + update_window_width() + check_and_index_strip_labels() + +def handle_transport_zone_change(transport): + if transport.zone is not None and transport.center_index is None: + if transport in transports: + transport.center_index = transports.index(transport) + for layout in [hstack_transport_center, hstack_transport_zone_left, hstack_transport_zone_right]: + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget() and hasattr(item.widget(), 'uid') and item.widget().uid == transport.uid: + layout.removeWidget(transport) + transport.setParent(None) + break + if transport.zone == "left": + transport.zone_index = hstack_transport_zone_left.count() + hstack_transport_zone_left.addWidget(transport) + vsep_transport_left.setVisible(True) + elif transport.zone == "right": + transport.zone_index = hstack_transport_zone_right.count() + hstack_transport_zone_right.addWidget(transport) + vsep_transport_right.setVisible(True) + else: + idx = transport.center_index if transport.center_index is not None else hstack_transport_center.count() + hstack_transport_center.insertWidget(min(idx, hstack_transport_center.count()), transport) + transport.center_index = None + transport.zone_index = None + vsep_transport_left.setVisible(any(t.zone == "left" for t in transports)) + vsep_transport_right.setVisible(any(t.zone == "right" for t in transports)) + check_and_index_strip_labels() + +def _update_fader_placeholder(): + if placeholder_fader_empty is not None: + placeholder_fader_empty.setVisible(len(faders) == 0) + +def _update_toggle_placeholder(): + if placeholder_toggle_empty is not None: + placeholder_toggle_empty.setVisible(len(toggles) == 0) + +def toggle_add(insert_after=None): + n = len(toggles) + 1 + toggle = ToggleWidget(f"Toggle {n}", default_cc=20) + if insert_after is not None and insert_after in toggles: + idx = toggles.index(insert_after) + toggles.insert(idx + 1, toggle) + hstack_toggle_center.insertWidget(idx + 1, toggle) + else: + toggles.append(toggle) + hstack_toggle_center.addWidget(toggle) + toggle.plus_btn.clicked.connect(lambda: toggle_add(insert_after=toggle)) + toggle.minus_btn.clicked.connect(lambda: toggle_remove(toggle)) + toggle.on_select_callback = check_and_update_selected_strip + toggle.on_zone_change_callback = handle_toggle_zone_change + toggle.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(toggle)) + update_window_width() + check_and_index_strip_labels() + _update_toggle_placeholder() + +def toggle_remove(toggle): + global selected_strip + if len(toggles) <= 0: + return + if selected_strip == toggle: + selected_strip = None + label_selected_channel.setText("Software item selected: None") + hstack_toggle_center.removeWidget(toggle) + toggle.deleteLater() + toggles.remove(toggle) + update_window_width() + check_and_index_strip_labels() + _update_toggle_placeholder() + +def fader_add(insert_after=None): + n = len(faders) + 1 + fader = FaderWidget(f"Fader {n}", default_cc=1) + if insert_after is not None and insert_after in faders: + idx = faders.index(insert_after) + faders.insert(idx + 1, fader) + hstack_fader_center.insertWidget(idx + 1, fader) + else: + faders.append(fader) + hstack_fader_center.addWidget(fader) + fader.plus_btn.clicked.connect(lambda: fader_add(insert_after=fader)) + fader.minus_btn.clicked.connect(lambda: fader_remove(fader)) + fader.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(fader)) + fader.on_select_callback = check_and_update_selected_strip + fader.on_zone_change_callback = handle_fader_zone_change + update_window_width() + check_and_index_strip_labels() + _update_fader_placeholder() + +def fader_remove(fader): + global selected_strip + if len(faders) <= 0: + return + if selected_strip == fader: + selected_strip = None + label_selected_channel.setText("Software item selected: None") + hstack_fader_center.removeWidget(fader) + fader.deleteLater() + faders.remove(fader) + update_window_width() + check_and_index_strip_labels() + _update_fader_placeholder() + +def transport_toggle_add(label, default_cc, insert_after=None): + t = TransportWidget(label, default_cc) + t.on_select_callback = lambda strip: check_and_update_selected_strip(strip) + t.learn_btn.clicked.connect(lambda: check_and_start_midi_learn(t)) + t.on_zone_change_callback = lambda strip: handle_transport_zone_change(strip) + t.plus_btn.clicked.connect(lambda: transport_toggle_add(f"T{len(transports)+1}", 20, insert_after=t)) + t.minus_btn.clicked.connect(lambda: transport_remove(t)) + params_visible = not transport_show_hide_config + for attr in ['learn_btn', 'cc_input_lbl', 'cc_spin', 'cc_output_lbl', 'midi_btn', 'osc_btn', + 'momentary_btn', 'toggle_mode_btn', 'osc_addr_edit', 'zone_btn', 'minus_btn', + 'plus_btn', 'title_edit', 'id_sep', 'id_label', 'color_swatch_btn']: + getattr(t, attr).setVisible(params_visible) + if insert_after is not None and insert_after in transports: + idx = transports.index(insert_after) + transports.insert(idx + 1, t) + update_window_width() + hstack_transport_center.insertWidget(idx + 1, t) + else: + transports.append(t) + update_window_width() + hstack_transport_center.addWidget(t) + check_and_index_strip_labels() + +def transport_remove(t): + global selected_strip + if len(transports) <= 1: + return + if selected_strip == t: + selected_strip = None + label_selected_channel.setText("Software item selected: None") + for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]: + layout.removeWidget(t) + t.deleteLater() + transports.remove(t) + vsep_transport_left.setVisible(any(tr.zone == "left" for tr in transports)) + vsep_transport_right.setVisible(any(tr.zone == "right" for tr in transports)) + update_window_width() + check_and_index_strip_labels() + +def load_transport_preset(): + states = loaded_presets.get("transport_preset", []) + if not states: + return + for t in list(transports): + for layout in [hstack_transport_zone_left, hstack_transport_center, hstack_transport_zone_right]: + layout.removeWidget(t) + t.deleteLater() + transports.clear() + for state in states: + transport_toggle_add(state.get("label", "T"), state.get("cc", 20)) + transports[-1].set_state(state) + handle_transport_zone_change(transports[-1]) + +def save_transport_preset(): + loaded_presets["transport_preset"] = [t.get_state() for t in transports] + +def save_io_config(): + loaded_presets["io_config"] = { + "midi_out": picker_midi_out.currentText(), + "midi_in": picker_midi_in.currentText(), + "transport_out": picker_transport_out.currentText(), + "osc_ip": textbox_osc_daw_ip.text(), + "osc_listen_port": spinner_osc_listen_port.value(), + "osc_send_port": spinner_osc_send_port.value(), + } + save_presets_file(loaded_presets) + +def load_io_config(): + global available_ports_midi_in + cfg = loaded_presets.get("io_config", {}) + if not cfg: + return + midi_out = cfg.get("midi_out", "") + picker_midi_out.setCurrentText(midi_out if midi_out in available_ports else "Not Connected") + transport_out = cfg.get("transport_out", "") + picker_transport_out.setCurrentText(transport_out if transport_out in available_ports else "Not Connected") + midi_in = cfg.get("midi_in", "") + input_names = [entry[0] for entry in available_ports_midi_in] + picker_midi_in.setCurrentText(midi_in if midi_in in input_names else "Not Connected") + ip = cfg.get("osc_ip", "") + if ip: + textbox_osc_daw_ip.setText(ip) + listen = cfg.get("osc_listen_port", 0) + if listen: + spinner_osc_listen_port.setValue(listen) + send = cfg.get("osc_send_port", 0) + if send: + spinner_osc_send_port.setValue(send) + +def preset_build_snapshot(name): + save_transport_preset() + return { + "faders": [f.get_state() for f in faders], + "toggles": [t.get_state() for t in toggles], + "browser": { + "back_cc": spinner_preset_browser_back.value(), + "fwd_cc": spinner_preset_browser_fwd.value() + }, + "preset_uuid": label_preset_uuid.text().strip(), + "show_hide": { + "fader": fader_show_hide_config, + "toggle": toggle_show_hide_config, + "transport": transport_show_hide_config, + } + } + + +def preset_load(preset): + fader_states = preset.get("faders", []) + while len(faders) < len(fader_states): + fader_add() + while len(faders) > len(fader_states): + widget = faders.pop() + hstack_fader_center.removeWidget(widget) + widget.deleteLater() + update_window_width() + _update_fader_placeholder() + for i, widget in enumerate(faders): + widget.set_state(fader_states[i]) + + center_faders = sorted( + [f for f in faders if not f.zone], + key=lambda f: f.center_index if f.center_index is not None else 999 + ) + for f in center_faders: + hstack_fader_center.removeWidget(f) + f.setParent(None) + for f in center_faders: + hstack_fader_center.addWidget(f) + + left_faders = sorted( + [f for f in faders if f.zone == "left"], + key=lambda f: f.zone_index if f.zone_index is not None else 999 + ) + right_faders = sorted( + [f for f in faders if f.zone == "right"], + key=lambda f: f.zone_index if f.zone_index is not None else 999 + ) + for f in left_faders: + hstack_fader_center.removeWidget(f) + f.setParent(None) + hstack_fader_zone_left.addWidget(f) + for f in right_faders: + hstack_fader_center.removeWidget(f) + f.setParent(None) + hstack_fader_zone_right.addWidget(f) + + update_window_width() + + if "toggles" in preset: + toggle_states = preset["toggles"] + elif "toggle" in preset: + toggle_states = [preset["toggle"]] + else: + toggle_states = [] + + while len(toggles) < len(toggle_states): + toggle_add() + while len(toggles) > len(toggle_states): + widget = toggles.pop() + hstack_toggle_center.removeWidget(widget) + widget.deleteLater() + update_window_width() + _update_toggle_placeholder() + for i, widget in enumerate(toggles): + widget.set_state(toggle_states[i]) + + center_toggles = sorted( + [t for t in toggles if not t.zone], + key=lambda t: t.center_index if t.center_index is not None else 999 + ) + for t in center_toggles: + hstack_toggle_center.removeWidget(t) + t.setParent(None) + for t in center_toggles: + hstack_toggle_center.addWidget(t) + + left_toggles = sorted( + [t for t in toggles if t.zone == "left"], + key=lambda t: t.zone_index if t.zone_index is not None else 999 + ) + right_toggles = sorted( + [t for t in toggles if t.zone == "right"], + key=lambda t: t.zone_index if t.zone_index is not None else 999 + ) + for t in left_toggles: + hstack_toggle_center.removeWidget(t) + t.setParent(None) + hstack_toggle_zone_left.addWidget(t) + for t in right_toggles: + hstack_toggle_center.removeWidget(t) + t.setParent(None) + hstack_toggle_zone_right.addWidget(t) + + browser = preset.get("browser", {}) + spinner_preset_browser_back.setValue(browser.get("back_cc", 22)) + spinner_preset_browser_fwd.setValue(browser.get("fwd_cc", 23)) + + show_hide = preset.get("show_hide", {}) + fader_show_hide_config_set(show_hide.get("fader", False)) + button_fader_show_hide_config.setText("Show Parameters" if show_hide.get("fader", False) else "Hide Parameters") + button_fader_show_hide_config.setChecked(show_hide.get("fader", False)) + + toggle_show_hide_config_set(show_hide.get("toggle", False)) + button_toggle_show_hide_config.setText("Show Parameters" if show_hide.get("toggle", False) else "Hide Parameters") + button_toggle_show_hide_config.setChecked(show_hide.get("toggle", False)) + + label_preset_uuid.setText(preset.get("preset_uuid", "")) + check_and_index_strip_labels() + +def export_preset(): + import json as _json + from PyQt6.QtWidgets import QFileDialog, QMessageBox + msg = QMessageBox(app_window) + msg.setWindowTitle("Export") + msg.setText("Export current preset or all presets?") + msg.setIcon(QMessageBox.Icon.NoIcon) + btn_current = msg.addButton("Current", QMessageBox.ButtonRole.AcceptRole) + btn_all = msg.addButton("All", QMessageBox.ButtonRole.AcceptRole) + msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole) + msg.exec() + clicked = msg.clickedButton() + if clicked is btn_current: + name = menuPresets.currentText() + if not name or name not in loaded_presets["presets"]: + return + default_path = os.path.join(os.path.expanduser("~"), f"{name}.json") + path, _ = QFileDialog.getSaveFileName(app_window, "Export Preset", default_path, "JSON Files (*.json)") + if not path: + return + with open(path, "w") as f: + _json.dump(loaded_presets["presets"][name], f, indent=2) + show_save_status() + elif clicked is btn_all: + default_path = os.path.join(os.path.expanduser("~"), "presets.json") + path, _ = QFileDialog.getSaveFileName(app_window, "Export All Presets", default_path, "JSON Files (*.json)") + if not path: + return + with open(path, "w") as f: + _json.dump(loaded_presets["presets"], f, indent=2) + show_save_status() + +def import_preset(): + import json as _json + from PyQt6.QtWidgets import QFileDialog, QInputDialog + path, _ = QFileDialog.getOpenFileName(app_window, "Import Preset", "", "JSON Files (*.json)") + if not path: + return + with open(path, "r") as f: + data = _json.load(f) + default_name = os.path.splitext(os.path.basename(path))[0] + name, ok = QInputDialog.getText(app_window, "Import Preset", "Preset name:", text=default_name) + if not ok or not name.strip(): + return + name = name.strip() + loaded_presets["presets"][name] = data + loaded_presets["__last_used__"] = name + save_presets_file(loaded_presets) + populate_preset_dropdown_menu() + menuPresets.setCurrentText(name) + preset_load(data) + show_save_status() + +def generate_preset_uuid(): + label_preset_uuid.setText(str(uuid.uuid4())) + save_preset(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_build=preset_build_snapshot, on_save_as=lambda: save_preset_as(app_window, menuPresets, loaded_presets, on_save=show_save_status, on_refresh=populate_preset_dropdown_menu, on_build=preset_build_snapshot)) + +def copy_preset_uuid(): + text = label_preset_uuid.text().strip() + if text: + QApplication.clipboard().setText(text) + +def switch_preset_by_uuid(uuid_str): + global uuid_presets_matched, uuid_presets_received_values, _track_uuid_check_pending + if not uuid_str.strip(): + return + _track_uuid_check_pending = False + if not uuid_presets_received_values: + labelStatus.setText("") + labelStatus.setStyleSheet("color: #00e676; font-style: italic;") + uuid_presets_received_values.append(uuid_str.strip()) + for name, preset in loaded_presets["presets"].items(): + if preset.get("preset_uuid", "") == uuid_str.strip(): + uuid_presets_matched.append(name) + break + uuid_presets_batch_timer.start(200) + +def resolve_uuid_batch(): + global uuid_presets_matched, uuid_presets_received_values + uuid_presets_matched = list(dict.fromkeys(uuid_presets_matched)) + if len(uuid_presets_matched) == 1: + menuPresets.setCurrentText(uuid_presets_matched[0]) + track_name = label_osc_track.text() + label_osc_preset.setText(uuid_presets_matched[0]) + label_osc_status.setText(f"DAW Track Selected: {track_name}") + label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;") + ui_show_rows_and_widgets() + elif len(uuid_presets_matched) > 1: + label_osc_status.setText("MULTIPLE PRESET IDs DETECTED") + label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") + ui_show_uuid_error_overlay("MULTIPLE PRESET IDs DETECTED", "#ff4444") + QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) + else: + label_osc_status.setText("NO MATCHING ID IN PRESET LIST") + label_osc_status.setStyleSheet("color: #ff4444; font-size: 11px; font-style: italic;") + ui_show_uuid_error_overlay("NO MATCHING ID IN PRESET LIST", "#ff4444") + QTimer.singleShot(4000, lambda: (label_osc_status.setText(""), label_osc_status.setStyleSheet("color: #00e676; font-size: 11px; font-style: italic;"))) + uuid_presets_matched.clear() + uuid_presets_received_values.clear() + +def check_preset_previous_and_assign(): + last = loaded_presets.get("__last_used__") + if last and last in loaded_presets["presets"]: + menuPresets.blockSignals(True) + menuPresets.setCurrentText(last) + menuPresets.blockSignals(False) + preset_load(loaded_presets["presets"][last]) + print(f"Restored last used preset: {last}") + +def check_midi_port_avail_and_assign(display_name): + + global available_ports_midi_in + global midi_in_port + + if not display_name or display_name == "Not Connected": + if midi_in_port is not None: + close_midi_input_port(midi_in_port) + midi_in_port = None + label_connection_midi_in.setText("Not Connected") + label_connection_midi_in.setStyleSheet("color: #888;") + return + port_entry = next((entry for entry in available_ports_midi_in if entry[0] == display_name), None) + if port_entry is None: + return + display_name, original_index, available = port_entry + if not available: + label_connection_midi_in.setText("In use by another program") + label_connection_midi_in.setStyleSheet("color: #e67e22;") + return + if midi_in_port is not None: + close_midi_input_port(midi_in_port) + midi_in_port = None + try: + midi_in_port = open_midi_input_port(original_index, route_midi_message_input) + label_connection_midi_in.setText("Connected") + label_connection_midi_in.setStyleSheet("color: #27ae60;") + print(f"Opened input port: {display_name}") + save_io_config() + except Exception as e: + print(f"Failed to open port {display_name}: {e}") + label_connection_midi_in.setText("Failed to open") + label_connection_midi_in.setStyleSheet("color: #c0392b;") + + +def route_midi_message_input(event, data=None): + global learning_strip + + message, _ = event + status = message[0] & 0xF0 + channel = (message[0] & 0x0F) + 1 + cc_num = message[1] if len(message) >= 2 else None + cc_val = message[2] if len(message) >= 3 else None + + bound_fader = None + bound_transport = None + if status == 0xB0: + for fader in faders: + if fader.hw_cc == cc_num: + bound_fader = fader + break + if not bound_fader: + for toggle in toggles: + if hasattr(toggle, 'hw_cc') and toggle.hw_cc == cc_num: + bound_fader = toggle + break + if not bound_fader: + for t in transports: + if hasattr(t, 'hw_cc') and t.hw_cc == cc_num: + bound_transport = t + break + + if status == 0xB0 and learning_strip: + learning_strip.bind_hw_cc(cc_num, channel=channel) + learning_strip = None + return + + if bound_transport: + print(f"[transport hw] CC{cc_num} val={cc_val} → uid={bound_transport.uid}") + midi_receiver.hw_cc_signal.emit(bound_transport.uid, cc_val) + elif bound_fader: + print(f"[hw] CC{cc_num} val={cc_val} → uid={bound_fader.uid} out_cc={bound_fader.cc_spin.value()}") + midi_receiver.hw_cc_signal.emit(bound_fader.uid, cc_val) + else: + print(f"[passthrough] CC{cc_num} val={cc_val}") + if midi_out.is_port_open(): + midi_out.send_message(message) + + midi_receiver.midi_in_signal.emit(list(message)) + +def midi_message_route_hw_to_output_cc(fader_uid, value): + for fader in faders: + if fader.uid == fader_uid: + out_cc = fader.cc_spin.value() + out_ch = fader.ch_spin.value() + print(f"[convert] HW CC{fader.hw_cc} CH{fader.hw_channel} ({value}) → CC{out_cc} CH{out_ch} ({value})") + fader.fader.blockSignals(True) + fader.fader.setValue(value) + fader.fader.blockSignals(False) + fader.cc_input_lbl.setText(fader._cc_input_label(value)) + fader.cc_output_lbl.setText(fader._cc_output_label(value)) + fader.readout.setText(f"CC {out_cc} → {value}") + fader.last_sent = value + fader.prev_position = value + send_cc(out_cc, value, channel=out_ch) + return + for toggle in toggles: + if toggle.uid == fader_uid: + out_cc = toggle.cc_spin.value() + out_ch = toggle.ch_spin.value() + print(f"[toggle convert] HW CC{toggle.hw_cc} CH{toggle.hw_channel} ({value}) → CC{out_cc} CH{out_ch} ({value})") + should_be_on = value > 63 + if toggle.toggle_state != should_be_on: + toggle.toggle_state = should_be_on + out_val = 127 if should_be_on else 0 + send_cc(out_cc, out_val, channel=out_ch) + toggle.cc_input_lbl.setText(toggle._cc_input_label(value)) + toggle.cc_output_lbl.setText(toggle._cc_output_label(out_val)) + toggle._update_btn_style() + return + for t in transports: + if t.uid == fader_uid: + out_cc = t.cc_spin.value() + should_be_on = value > 63 + if t.toggle_state != should_be_on: + t.toggle_state = should_be_on + out_val = 127 if should_be_on else 0 + send_transport_cc(out_cc, out_val) + t.cc_input_lbl.setText(f"In CC{t.hw_cc} [{value}]") + t.cc_output_lbl.setText(f"Out CC{out_cc} [{out_val}]") + t._update_btn_style() + return + +def log_ui_show_hide(checked): + container_osc_in_log.setVisible(checked) + container_osc_out_log.setVisible(checked) + log_ui_window_calc() + + +def log_ui_window_calc(): + geo = app_window.frameGeometry() + h = geo.height() + container_osc_in_log.setFixedHeight(h) + container_osc_in_log.move(geo.right() + 4, geo.top()) + container_osc_out_log.setFixedWidth(360) + container_osc_out_log.setFixedHeight(h // 2) + container_osc_out_log.move(geo.right() + 4, geo.top() + h // 2 + 4) + container_osc_in_log.setFixedHeight(h // 2 - 4) + +def start_osc_server(port=9000): + global _osc_server, _osc_thread + stop_osc_server() + + d = osc_dispatcher.Dispatcher() + + def emit_raw(addr, *args): + val_str = " ".join(str(a) for a in args) if args else "" + osc_receiver.raw_message_signal.emit(addr, val_str) + def on_play(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.play_signal.emit(int(args[0])) + def on_record(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.record_signal.emit(int(args[0])) + def on_time(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.position_signal.emit(str(args[0])) + def on_bar(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.bar_signal.emit(str(args[0])) + def on_track_name(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.track_name_signal.emit(str(args[0])) + def on_preset(addr, *args): + emit_raw(addr, *args) + if args: osc_receiver.preset_name_signal.emit(str(args[0])) + def on_default(addr, *args): + emit_raw(addr, *args) + if 'insertname' in addr and args: + osc_receiver.uuid_preset.emit(str(args[0])) + if 'trackname' in addr and args: + osc_receiver.track_name_signal.emit(str(args[0])) + + d.map("/play", on_play) + d.map("/record", on_record) + d.map("/*/time", on_time) + d.map("/*/bar", on_bar) + d.map("/track/*", on_track_name) + d.map("/fx/*", on_preset) + d.map("/fxname", on_preset) + d.set_default_handler(on_default) + + try: + _osc_server = osc_server.ThreadingOSCUDPServer(("0.0.0.0", port), d) + _osc_thread = threading.Thread(target=_osc_server.serve_forever, daemon=True) + _osc_thread.start() + print(f"[OSC] Listening on port {port}") + return True + except Exception as e: + print(f"[OSC] Failed to start: {e}") + _osc_server = None + return False + +def ui_show_rows_and_widgets(): + container_preset_browser.setVisible(True) + container_rows_and_widgets.setVisible(True) + view_uuid_error_overlay.setVisible(False) + +def ui_show_uuid_error_overlay(message, color): + label_overlay_message.setText(message) + label_overlay_message.setStyleSheet(f"color: {color}; font-size: 18px; font-weight: bold;") + container_preset_browser.setVisible(False) + container_rows_and_widgets.setVisible(False) + view_uuid_error_overlay.setVisible(True) + +def stop_osc_server(): + global _osc_server, _osc_thread + if _osc_server: + _osc_server.shutdown() + _osc_server = None + _osc_thread = None + +def set_app_window(window: QMainWindow): + global app_window + app_window = window + +# MARK: FADER SHOW HIDE CONFIG +def fader_show_hide_config_set(enabled): + global fader_show_hide_config + fader_show_hide_config = enabled + for fader in faders: + if enabled: + fader.setFixedHeight(fader.height()) else: - key = event.key() - modifiers = event.modifiers() - for name, entry in self.transport_buttons.items(): - if entry["key"] is None: - continue - required_modifier = entry.get("modifier", Qt.KeyboardModifier.NoModifier) - if key == entry["key"] and modifiers == required_modifier: - self.fire_transport(name) - return - super().keyPressEvent(event) + fader.setMinimumHeight(0) + fader.setMaximumHeight(16777215) + fader.learn_btn.setVisible(not enabled) + fader.cc_spin_row.setVisible(not enabled) + fader.ch_spin_row.setVisible(not enabled) + fader.pickup_checkbox.setVisible(not enabled) + fader.zone_btn.setVisible(not enabled) + fader.plus_btn.setVisible(not enabled) + fader.minus_btn.setVisible(not enabled) + +def fader_show_hide_config_toggle(): + global fader_show_hide_config + enabled = not fader_show_hide_config + fader_show_hide_config_set(enabled) + button_fader_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters") + button_fader_show_hide_config.setChecked(enabled) + + # MARK: TRANSPORT SHOW HIDE CONFIG +def transport_show_hide_config_set(enabled): + global transport_show_hide_config + transport_show_hide_config = enabled + loaded_presets["transport_show_hide"] = enabled + save_presets_file(loaded_presets) + button_transport_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters") + button_transport_show_hide_config.setChecked(enabled) + for t in transports: + t.learn_btn.setVisible(not enabled) + t.cc_input_lbl.setVisible(not enabled) + t.cc_spin.setVisible(not enabled) + t.cc_output_lbl.setVisible(not enabled) + t.midi_btn.setVisible(not enabled) + t.osc_btn.setVisible(not enabled) + t.momentary_btn.setVisible(not enabled) + t.toggle_mode_btn.setVisible(not enabled) + t.osc_addr_edit.setVisible(not enabled) + t.zone_btn.setVisible(not enabled) + t.minus_btn.setVisible(not enabled) + t.plus_btn.setVisible(not enabled) + t.title_edit.setVisible(not enabled) + t.id_sep.setVisible(not enabled) + t.id_label.setVisible(not enabled) + t.color_swatch_btn.setVisible(not enabled) +def transport_show_hide_config_toggle(): + global transport_show_hide_config + transport_show_hide_config_set(not transport_show_hide_config) + +# MARK: TOGGLE SHOW HIDE CONFIG +def toggle_show_hide_config_set(enabled): + global toggle_show_hide_config + toggle_show_hide_config = enabled + for t in toggles: + t.grp_learn.setVisible(not enabled) + t.arrow_lbl.setVisible(not enabled) + t.grp_cc.setVisible(not enabled) + t.zone_btn.setVisible(not enabled) + t.minus_btn.setVisible(not enabled) + t.plus_btn.setVisible(not enabled) + t.id_label.setVisible(not enabled) + t.color_swatch_btn.setVisible(not enabled) + +def toggle_show_hide_config_toggle(): + global toggle_show_hide_config + enabled = not toggle_show_hide_config + toggle_show_hide_config_set(enabled) + button_toggle_show_hide_config.setText("Show Parameters" if enabled else "Hide Parameters") + button_toggle_show_hide_config.setChecked(enabled) diff --git a/midi_sender.py b/midi_sender.py index 85177d2..8dfd305 100644 --- a/midi_sender.py +++ b/midi_sender.py @@ -12,11 +12,11 @@ available_ports = midi_out.get_ports() print("Available MIDI ports:", available_ports) -def send_cc(cc_number, value): +def send_cc(cc_number, value, channel=None): + ch = (channel - 1) if channel is not None else MIDI_CH if midi_out.is_port_open(): - midi_out.send_message([0xB0 | MIDI_CH, cc_number, value]) + midi_out.send_message([0xB0 | ch, cc_number, value]) midi_receiver.midi_out_signal.emit() - #print([0xB0 | MIDI_CH, cc_number, value]) def send_transport_cc(cc_number, value): diff --git a/osc_receiver.py b/osc_receiver.py index 8c1386d..31fa12f 100644 --- a/osc_receiver.py +++ b/osc_receiver.py @@ -9,6 +9,8 @@ class OSCReceiver(QObject): record_signal = pyqtSignal(int) raw_message_signal = pyqtSignal(str, str) osc_out_signal = pyqtSignal(str, str) + uuid_preset = pyqtSignal(str) + bar_signal = pyqtSignal(str) osc_receiver = OSCReceiver() \ No newline at end of file diff --git a/osc_sender.py b/osc_sender.py index 29a45d5..da2417a 100644 --- a/osc_sender.py +++ b/osc_sender.py @@ -1,5 +1,4 @@ from pythonosc import udp_client -from midi_receiver import midi_receiver from osc_receiver import osc_receiver @@ -32,7 +31,6 @@ def set_osc_target(ip=None, port=None): def send_osc_message(address, value=1.0): try: get_osc_client().send_message(address, float(value)) - midi_receiver.transport_out_signal.emit() osc_receiver.osc_out_signal.emit(address, str(value)) print(f"[OSC] → {address} {value}") except Exception as e: diff --git a/presets.py b/presets.py index 90f5518..2034ea8 100644 --- a/presets.py +++ b/presets.py @@ -15,6 +15,7 @@ PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json") os.makedirs(PRESETS_DIR, exist_ok=True) + def load_presets_file(): if os.path.exists(PRESETS_FILE): with open(PRESETS_FILE, "r") as f: @@ -27,32 +28,42 @@ def save_presets_file(data): def ensure_preset_structure(presets): if "presets" not in presets: - return {"__last_used__": None, "presets": presets} + return {"__last_used__": None, "presets": presets, "transport_preset": [], "transport_show_hide": False, "io_config": {}} + if "transport_preset" not in presets: + presets["transport_preset"] = [] + if "transport_show_hide" not in presets: + presets["transport_show_hide"] = False + if "io_config" not in presets: + presets["io_config"] = {} return presets -def on_preset_selected(menu_presets: QComboBox, app_window: QMainWindow, ready: bool, loaded_presets: dict): +def on_preset_selected(menu_presets: QComboBox, app_window: QMainWindow, ready: bool, loaded_presets: dict, on_load=None): selectedOption = menu_presets.currentText() if ready and selectedOption and selectedOption in loaded_presets["presets"]: loaded_presets["__last_used__"] = selectedOption save_presets_file(loaded_presets) - app_window.apply_preset(loaded_presets["presets"][selectedOption]) + if on_load: + on_load(loaded_presets["presets"][selectedOption]) -def save_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict): + +def save_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_save=None, on_build=None, on_save_as=None): selectedOption = menu_presets.currentText() if not selectedOption or selectedOption not in loaded_presets["presets"]: - app_window.save_preset_as() # not self + if on_save_as: + on_save_as() return - loaded_presets["presets"][selectedOption] = app_window._build_preset_data(selectedOption) # not self + loaded_presets["presets"][selectedOption] = on_build(selectedOption) if on_build else {} loaded_presets["__last_used__"] = selectedOption save_presets_file(loaded_presets) - app_window._show_save_status() # not self + if on_save: + on_save() print(f"Saved preset: {selectedOption}") -def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict): +def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_save=None, on_refresh=None, on_build=None): print(f"save as button tapped") @@ -62,24 +73,25 @@ def save_preset_as(app_window: QMainWindow, menu_presets: QComboBox, loaded_pres if not ok or not selectedOption.strip(): return selectedOption = selectedOption.strip() - loaded_presets["presets"][selectedOption] = app_window._build_preset_data(selectedOption) + loaded_presets["presets"][selectedOption] = on_build(selectedOption) if on_build else {} loaded_presets["__last_used__"] = selectedOption save_presets_file(loaded_presets) - app_window.refresh_preset_combo() + if on_refresh: + on_refresh() menu_presets.setCurrentText(selectedOption) - app_window._show_save_status() + if on_save: + on_save() print(f"Saved preset as: {selectedOption}") - -def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict): - selectedOption = menu_presets.currentText() + +def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_presets: dict, on_refresh=None): selectedOption = menu_presets.currentText() if not selectedOption or selectedOption not in loaded_presets["presets"]: return - + msg = QMessageBox(app_window) msg.setWindowTitle("Remove Preset") msg.setText(f'Are you sure you want to remove "{selectedOption}"?') @@ -89,12 +101,13 @@ def delete_preset(app_window: QMainWindow, menu_presets: QComboBox, loaded_prese if reply != QMessageBox.StandardButton.Yes: return - + del loaded_presets["presets"][selectedOption] if loaded_presets["__last_used__"] == selectedOption: loaded_presets["__last_used__"] = None - + save_presets_file(loaded_presets) - app_window.refresh_preset_combo() + if on_refresh: + on_refresh() print(f"Deleted preset: {selectedOption}") \ No newline at end of file diff --git a/presets/presets.json b/presets/presets.json index 30d9a07..4e97fe6 100644 --- a/presets/presets.json +++ b/presets/presets.json @@ -1,1086 +1,439 @@ { - "__last_used__": "gay3", + "__last_used__": "preset1", "presets": { - "1": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", + "preset1": { "faders": [ { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", + "uid": "c98d2009-d186-489a-8883-ccbd612cb4af", "label": "Fader 1", "cc": 1, + "ch": 1, + "value": 38, + "color": "Red", + "pickup": false, + "last_sent": 38, + "hw_cc": null, + "hw_channel": null, + "zone": null, + "center_index": null, + "zone_index": null + }, + { + "uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095", + "label": "Fader 2", + "cc": 1, + "ch": 1, + "value": 64, + "color": "Gray", + "pickup": false, + "last_sent": 64, + "hw_cc": null, + "hw_channel": null, + "zone": "right", + "center_index": 1, + "zone_index": -1 + }, + { + "uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790", + "label": "Fader 3", + "cc": 1, + "ch": 1, "value": 64, "color": "Orange", "pickup": false, "last_sent": 64, "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", - "label": "Fader 2", - "cc": 1, - "value": 64, - "color": "Yellow", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", - "label": "Fader 3", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", - "label": "Fader 4", - "cc": 1, - "value": 97, - "color": "Gray", - "pickup": false, - "last_sent": 97, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null + "hw_channel": null, + "zone": "left", + "center_index": 2, + "zone_index": -1 } ], "toggles": [ { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", + "uid": "b4159deb-758b-4711-838c-a6bfe6d59872", "label": "Toggle 1", "cc": 20, + "ch": 1, "state": false, "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, + "zone": "left", + "center_index": 0, + "zone_index": -1, "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null }, { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, + "uid": "73962e37-53b3-400f-a7e2-25ef7f1ea583", + "label": "Toggle 2", + "cc": 20, + "ch": 1, "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, + "color": "Blue", + "zone": "left", + "center_index": 1, + "zone_index": 0, "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null }, { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, + "uid": "b3cf419a-5183-4272-bd6f-44f858afbec8", + "label": "Toggle 3", + "cc": 20, + "ch": 1, "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, + "color": "Yellow", + "zone": "left", + "center_index": 2, + "zone_index": 1, "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null }, { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, + "uid": "c179bfea-deec-416a-b4aa-74e81b5cbe03", + "label": "Toggle 4", + "cc": 20, + "ch": 1, + "state": false, + "color": "Orange", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null + }, + { + "uid": "a5a50618-fc4c-446e-868e-e4eafa6254e7", + "label": "Toggle 6", + "cc": 20, + "ch": 1, "state": false, "color": "Gray", "zone": null, "center_index": null, "zone_index": null, "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null + }, + { + "uid": "f1775380-3390-4fbb-bd8b-8c30ff18f72a", + "label": "Toggle 7", + "cc": 20, + "ch": 1, + "state": false, + "color": "Gray", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "hw_channel": null + }, + { + "uid": "2064f2ca-6d18-468b-bf45-9bcb1fa1247d", + "label": "Toggle 5", + "cc": 20, + "ch": 1, + "state": false, + "color": "Pink", + "zone": "right", + "center_index": 4, + "zone_index": -1, + "hw_cc": null, + "hw_channel": null } ], "browser": { "back_cc": 22, "fwd_cc": 23 }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 + "preset_uuid": "f230a4fd-edc3-4c4b-8585-f73c82168fee", + "show_hide": { + "fader": false, + "toggle": false, + "transport": false } }, - "2": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", - "faders": [ - { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", - "label": "Fader 1", - "cc": 1, - "value": 64, - "color": "Orange", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", - "label": "Fader 2", - "cc": 1, - "value": 64, - "color": "Yellow", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", - "label": "Fader 3", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", - "label": "Fader 4", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8", - "label": "Fader 5", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5", - "label": "Fader 6", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - } - ], + "preset2": { + "faders": [], "toggles": [ { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", + "uid": "6e8e21a9-0d01-482f-b5cc-8551bfd9c717", "label": "Toggle 1", "cc": 20, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, + "ch": 1, "state": false, "color": "Gray", "zone": null, "center_index": null, "zone_index": null, "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null } ], "browser": { "back_cc": 22, "fwd_cc": 23 }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 + "preset_uuid": "30ff84d1-9240-41f6-b499-1d18ef7ab2eb", + "show_hide": { + "fader": false, + "toggle": false, + "transport": false } }, - "gay": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", + "preset3": { "faders": [ { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", + "uid": "c98d2009-d186-489a-8883-ccbd612cb4af", "label": "Fader 1", "cc": 1, + "ch": 1, "value": 64, - "color": "Orange", + "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null + "hw_channel": null, + "zone": "left", + "center_index": 0, + "zone_index": -1 }, { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", + "uid": "7bf9e219-b7a6-4d8e-b317-da9eb18a8095", "label": "Fader 2", "cc": 1, + "ch": 1, "value": 64, - "color": "Yellow", + "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", + "uid": "bd0c9b6a-db81-411d-88b0-5ebae3c4e790", "label": "Fader 3", "cc": 1, + "ch": 1, "value": 64, "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", - "label": "Fader 4", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8", - "label": "Fader 5", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5", - "label": "Fader 6", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - } - ], - "toggles": [ - { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", - "label": "Toggle 1", - "cc": 20, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" + "hw_channel": null, + "zone": "right", + "center_index": 2, + "zone_index": -1 } ], + "toggles": [], "browser": { "back_cc": 22, "fwd_cc": 23 }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 + "preset_uuid": "e658b6bf-4bb7-4d68-bd8b-07775ee80c53", + "show_hide": { + "fader": false, + "toggle": false, + "transport": false } }, - "gay2": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", + "whatever": { + "faders": [], + "toggles": [], + "preset_uuid": "", + "browser": {}, + "show_hide": {} + }, + "preset5": { "faders": [ { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", + "uid": "14ec66c2-6a0a-418e-9240-776cb13f6e2e", "label": "Fader 1", "cc": 1, + "ch": 1, "value": 64, - "color": "Orange", + "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", + "uid": "5aaecf41-3455-4fa2-bef1-ca02f5933a69", "label": "Fader 2", "cc": 1, + "ch": 1, "value": 64, - "color": "Yellow", + "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", + "uid": "e8e8cca8-3bad-49a5-8c91-d3a78d25a1eb", "label": "Fader 3", "cc": 1, + "ch": 1, "value": 64, "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", + "uid": "ae9cd47a-96af-45a4-83e6-816916a65e0e", "label": "Fader 4", "cc": 1, + "ch": 1, "value": 64, "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8", + "uid": "fee53fde-2f59-4800-8873-3d75ca619366", "label": "Fader 5", "cc": 1, + "ch": 1, "value": 64, - "color": "Blue", + "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5", + "uid": "1783a98d-16c9-4a06-ae4d-0f6ab36d4421", "label": "Fader 6", "cc": 1, - "value": 109, - "color": "Blue", + "ch": 1, + "value": 64, + "color": "Gray", "pickup": false, - "last_sent": 109, + "last_sent": 64, "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null }, { - "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5", + "uid": "2a55bdef-83aa-458f-a37d-e12a61e3e6a0", "label": "Fader 7", "cc": 1, + "ch": 1, "value": 64, "color": "Gray", "pickup": false, "last_sent": 64, "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a77ac937-8661-4639-8091-bba5275d971f", - "label": "Fader 8", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "8e852e9a-a579-4c45-b260-a1311b144b12", - "label": "Fader 9", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, + "hw_channel": null, "zone": null, "center_index": null, "zone_index": null } ], - "toggles": [ - { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", - "label": "Toggle 1", - "cc": 20, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - } - ], + "toggles": [], "browser": { "back_cc": 22, "fwd_cc": 23 }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 - } - }, - "gay3": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", - "faders": [ - { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", - "label": "Fader 1", - "cc": 1, - "value": 64, - "color": "Orange", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", - "label": "Fader 2", - "cc": 1, - "value": 64, - "color": "Yellow", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", - "label": "Fader 3", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", - "label": "Fader 4", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8", - "label": "Fader 5", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5", - "label": "Fader 6", - "cc": 1, - "value": 109, - "color": "Blue", - "pickup": false, - "last_sent": 109, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5", - "label": "Fader 7", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a77ac937-8661-4639-8091-bba5275d971f", - "label": "Fader 8", - "cc": 1, - "value": 94, - "color": "Yellow", - "pickup": false, - "last_sent": 94, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "8e852e9a-a579-4c45-b260-a1311b144b12", - "label": "Fader 9", - "cc": 1, - "value": 102, - "color": "Teal", - "pickup": false, - "last_sent": 102, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - } - ], - "toggles": [ - { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", - "label": "Toggle 1", - "cc": 20, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - }, - { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "midi", - "osc_addr": "/play", - "trigger_mode": "momentary" - } - ], - "browser": { - "back_cc": 22, - "fwd_cc": 23 - }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 - } - }, - "gay5": { - "midi_port": "IAC Driver Bus 1", - "midi_input": "IAC Driver Bus 1", - "transport_port": "IAC Driver Bus 1", - "faders": [ - { - "uid": "7aa48acf-7d6b-47fa-a8b4-e10223bb62c5", - "label": "Fader 1", - "cc": 1, - "value": 64, - "color": "Orange", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "2b5abf64-9465-4191-a0e3-f14f35fcaf71", - "label": "Fader 2", - "cc": 1, - "value": 64, - "color": "Yellow", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "aa1bd51a-f53a-4b54-8d1e-44667f7c5678", - "label": "Fader 3", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a0ce87e5-29fa-47b9-bd69-8f958c5f0cbc", - "label": "Fader 4", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "54bdc59c-1a87-4f95-a50d-7decadb6fdf8", - "label": "Fader 5", - "cc": 1, - "value": 64, - "color": "Blue", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "faaee6d8-1ec9-422a-a07e-385fc1cabbb5", - "label": "Fader 6", - "cc": 1, - "value": 109, - "color": "Blue", - "pickup": false, - "last_sent": 109, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "da37cac1-d0d9-4a18-bb94-8b7345bb8ee5", - "label": "Fader 7", - "cc": 1, - "value": 64, - "color": "Gray", - "pickup": false, - "last_sent": 64, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "a77ac937-8661-4639-8091-bba5275d971f", - "label": "Fader 8", - "cc": 1, - "value": 94, - "color": "Yellow", - "pickup": false, - "last_sent": 94, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - }, - { - "uid": "8e852e9a-a579-4c45-b260-a1311b144b12", - "label": "Fader 9", - "cc": 1, - "value": 102, - "color": "White", - "pickup": false, - "last_sent": 102, - "hw_cc": null, - "zone": null, - "center_index": null, - "zone_index": null - } - ], - "toggles": [ - { - "uid": "2f1773e4-9df2-4c14-9960-68eb1fe1c98e", - "label": "Toggle 1", - "cc": 20, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null - } - ], - "transports": [ - { - "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", - "label": "Play", - "cc": 117, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "osc", - "osc_addr": "/play", - "trigger_mode": "toggle" - }, - { - "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", - "label": "Stop", - "cc": 116, - "state": false, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "osc", - "osc_addr": "/play", - "trigger_mode": "toggle" - }, - { - "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", - "label": "Record", - "cc": 118, - "state": true, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "osc", - "osc_addr": "/play", - "trigger_mode": "toggle" - }, - { - "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", - "label": "Arm", - "cc": 119, - "state": true, - "color": "Gray", - "zone": null, - "center_index": null, - "zone_index": null, - "hw_cc": null, - "output_mode": "osc", - "osc_addr": "/play", - "trigger_mode": "toggle" - } - ], - "browser": { - "back_cc": 22, - "fwd_cc": 23 - }, - "osc": { - "ip": "127.0.0.1", - "listen_port": 9000, - "send_port": 8000 + "preset_uuid": "3e9d51d2-cf55-49f4-80a4-2f7f4466f959", + "show_hide": { + "fader": false, + "toggle": false, + "transport": false } } + }, + "transport_preset": [ + { + "uid": "9ee09db7-2cf5-4ae6-a154-b25bb262565c", + "label": "Play", + "cc": 117, + "state": false, + "color": "Green", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "output_mode": "osc", + "osc_addr": "t/1/play", + "trigger_mode": "momentary" + }, + { + "uid": "e2e8eb84-c2c9-4060-a818-b087ebd5aabd", + "label": "Stop", + "cc": 116, + "state": false, + "color": "Gray", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "output_mode": "osc", + "osc_addr": "t/1/stop", + "trigger_mode": "momentary" + }, + { + "uid": "6b5bcaf7-1d01-4ccd-b23a-ebae8da57a7f", + "label": "Record", + "cc": 118, + "state": false, + "color": "Red", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "output_mode": "osc", + "osc_addr": "t/1/record", + "trigger_mode": "momentary" + }, + { + "uid": "2a1fcb50-b9f2-4c33-bce0-5bd5cadd9c46", + "label": "Arm", + "cc": 119, + "state": false, + "color": "Red", + "zone": null, + "center_index": null, + "zone_index": null, + "hw_cc": null, + "output_mode": "osc", + "osc_addr": "t/1/recenable/1/@", + "trigger_mode": "momentary" + } + ], + "transport_show_hide": false, + "io_config": { + "midi_out": "Not Connected", + "midi_in": "VMPK Output", + "transport_out": "Not Connected", + "osc_ip": "127.0.0.1", + "osc_listen_port": 9000, + "osc_send_port": 8000 } } \ No newline at end of file diff --git a/togglewidget.py b/togglewidget.py index 46c8937..ad3310a 100644 --- a/togglewidget.py +++ b/togglewidget.py @@ -16,8 +16,8 @@ class ToggleWidget(QWidget): def __init__(self, label, default_cc): super().__init__() self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) - self.setFixedWidth(100) - self.current_color = "transparent" + self.setFixedWidth(115) + self.current_color = PALETTE_HEX.get("Gray", "#263238") self.toggle_state = False self.uid = str(uuid.uuid4()) self.zone = None @@ -38,33 +38,83 @@ class ToggleWidget(QWidget): inner.setSpacing(4) inner.setAlignment(Qt.AlignmentFlag.AlignHCenter) - # Learn button + _grp_style = "QFrame { border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; }" + + # Group 1: Learn + input readout self.hw_cc = None + self.hw_channel = None self.is_learning = False + self.grp_learn = QFrame() + self.grp_learn.setStyleSheet(_grp_style) + _grp1_layout = QVBoxLayout(self.grp_learn) + _grp1_layout.setContentsMargins(4, 4, 4, 4) + _grp1_layout.setSpacing(3) + self.learn_btn = QPushButton("Learn") self.learn_btn.setFixedHeight(20) + self.learn_btn.setStyleSheet("border: none;") self.learn_btn.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.learn_btn.customContextMenuRequested.connect(self.on_learn_context_menu) - inner.addWidget(self.learn_btn) + _grp1_layout.addWidget(self.learn_btn) - # CC In label self.cc_input_lbl = QLabel("") self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;") - inner.addWidget(self.cc_input_lbl) + self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;") + self.cc_input_lbl.setFixedHeight(16) + _grp1_layout.addWidget(self.cc_input_lbl) + + inner.addWidget(self.grp_learn) + + self.arrow_lbl = QLabel("▼") + self.arrow_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) + self.arrow_lbl.setStyleSheet("color: rgba(255,255,255,0.3); font-size: 9px; background: transparent;") + self.arrow_lbl.setFixedHeight(12) + inner.addWidget(self.arrow_lbl) + + # Group 2: CC spin + CH spin + output readout + self.grp_cc = QFrame() + self.grp_cc.setStyleSheet(_grp_style) + _grp2_layout = QVBoxLayout(self.grp_cc) + _grp2_layout.setContentsMargins(4, 4, 4, 4) + _grp2_layout.setSpacing(3) - # CC spin self.cc_spin = QSpinBox() - self.cc_spin.setMinimum(1) + self.cc_spin.setMinimum(0) self.cc_spin.setMaximum(127) self.cc_spin.setValue(default_cc) - self.cc_spin.setFixedWidth(75) - inner.addWidget(self.cc_spin, alignment=Qt.AlignmentFlag.AlignHCenter) + self.cc_spin.setFixedWidth(55) + _cc_lbl = QLabel("CC") + _cc_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;") + self.cc_spin_row = QWidget() + _hbox_cc = QHBoxLayout(self.cc_spin_row) + _hbox_cc.setContentsMargins(0, 0, 0, 0) + _hbox_cc.setSpacing(4) + _hbox_cc.addWidget(_cc_lbl) + _hbox_cc.addWidget(self.cc_spin) + _grp2_layout.addWidget(self.cc_spin_row) - self.cc_output_lbl = QLabel(f"Out CC{default_cc} [--]") + self.ch_spin = QSpinBox() + self.ch_spin.setMinimum(1) + self.ch_spin.setMaximum(16) + self.ch_spin.setValue(1) + self.ch_spin.setFixedWidth(55) + _ch_lbl = QLabel("CH") + _ch_lbl.setStyleSheet("color: #fff; font-size: 10px; border: none;") + self.ch_spin_row = QWidget() + _hbox_ch = QHBoxLayout(self.ch_spin_row) + _hbox_ch.setContentsMargins(0, 0, 0, 0) + _hbox_ch.setSpacing(4) + _hbox_ch.addWidget(_ch_lbl) + _hbox_ch.addWidget(self.ch_spin) + _grp2_layout.addWidget(self.ch_spin_row) + + self.cc_output_lbl = QLabel(f"CC{default_cc} [--]") self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") - inner.addWidget(self.cc_output_lbl) + self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;") + self.cc_output_lbl.setFixedHeight(16) + _grp2_layout.addWidget(self.cc_output_lbl) + + inner.addWidget(self.grp_cc) # Toggle button with LED dot overlay btn_container = QWidget() @@ -161,6 +211,17 @@ class ToggleWidget(QWidget): def set_selected(self, selected): self.apply_color(self.current_color, selected=selected) + def _cc_input_label(self, vel="--"): + if self.hw_cc is None: + return "" + ch = f" CH{self.hw_channel}" if self.hw_channel is not None else "" + return f"CC{self.hw_cc}{ch} [{vel}]" + + def _cc_output_label(self, vel="--"): + out_cc = self.cc_spin.value() + out_ch = self.ch_spin.value() + return f"CC{out_cc} CH{out_ch} [{vel}]" + def start_learn(self): self.is_learning = True self.learn_btn.setText("Listening...") @@ -168,18 +229,19 @@ class ToggleWidget(QWidget): def stop_learn(self): self.is_learning = False - self.learn_btn.setStyleSheet("") + self.learn_btn.setStyleSheet("border: none;") if self.hw_cc is not None: self.learn_btn.setText(f"HW: CC{self.hw_cc}") - self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]") - self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") + self.cc_input_lbl.setText(self._cc_input_label()) + self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;") else: self.learn_btn.setText("Learn") self.cc_input_lbl.setText("") - self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;") + self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px; border: none;") - def bind_hw_cc(self, cc_number): + def bind_hw_cc(self, cc_number, channel=None): self.hw_cc = cc_number + self.hw_channel = channel self.stop_learn() def on_learn_context_menu(self, pos): @@ -191,9 +253,10 @@ class ToggleWidget(QWidget): action = menu.exec(self.learn_btn.mapToGlobal(pos)) if action == clear_action: self.hw_cc = None + self.hw_channel = None self.is_learning = False self.learn_btn.setText("Learn") - self.learn_btn.setStyleSheet("") + self.learn_btn.setStyleSheet("border: none;") self.cc_input_lbl.setText("") def update_id_label(self, zone, index): @@ -229,9 +292,11 @@ class ToggleWidget(QWidget): def on_click(self): self.toggle_state = not self.toggle_state cc_num = self.cc_spin.value() + out_ch = self.ch_spin.value() value = 127 if self.toggle_state else 0 - send_cc(cc_num, value) - self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]") + print(f"[toggle] CC{cc_num} CH{out_ch} ({value})") + send_cc(cc_num, value, channel=out_ch) + self.cc_output_lbl.setText(self._cc_output_label(value)) self._update_btn_style() def _update_btn_style(self): @@ -245,12 +310,14 @@ class ToggleWidget(QWidget): "uid": self.uid, "label": self.title_edit.text(), "cc": self.cc_spin.value(), + "ch": self.ch_spin.value(), "state": self.toggle_state, "color": self.color_name, "zone": self.zone, "center_index": self.center_index, "zone_index": self.zone_index, - "hw_cc": self.hw_cc + "hw_cc": self.hw_cc, + "hw_channel": self.hw_channel, } def set_state(self, state): @@ -258,6 +325,7 @@ class ToggleWidget(QWidget): self.uid = state["uid"] self.title_edit.setText(state.get("label", "")) self.cc_spin.setValue(state.get("cc", 1)) + self.ch_spin.setValue(state.get("ch", 1)) color_name = state.get("color", "Gray") hex_color = PALETTE_HEX.get(color_name, "transparent") self.color_name = color_name @@ -265,18 +333,18 @@ class ToggleWidget(QWidget): self.apply_color(hex_color) self.toggle_state = state.get("state", False) self._update_btn_style() - cc_num = self.cc_spin.value() value = 127 if self.toggle_state else 0 - self.cc_output_lbl.setText(f"Out CC{cc_num} [{value}]") + self.cc_output_lbl.setText(self._cc_output_label(value)) self.zone = state.get("zone", None) self.center_index = state.get("center_index", None) self.zone_index = state.get("zone_index", None) self.zone_btn.set_state(self.zone == "left", self.zone == "right") self.hw_cc = state.get("hw_cc", None) + self.hw_channel = state.get("hw_channel", None) if self.hw_cc is not None: self.learn_btn.setText(f"HW: CC{self.hw_cc}") - self.cc_input_lbl.setText(f"In CC{self.hw_cc} [--]") - self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") + self.cc_input_lbl.setText(self._cc_input_label()) + self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px; border: none;") else: self.learn_btn.setText("Learn") self.cc_input_lbl.setText("") \ No newline at end of file diff --git a/transportwidget.py b/transportwidget.py index 2999adf..d59527c 100644 --- a/transportwidget.py +++ b/transportwidget.py @@ -4,7 +4,7 @@ from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSpinBox, QLineEdit, QFrame, QSizePolicy ) -from PyQt6.QtCore import Qt, QEvent +from PyQt6.QtCore import Qt, QEvent, QTimer from palette import PALETTE_HEX, PALETTE_VIVID from styles import TITLE_STYLE @@ -24,13 +24,13 @@ class TransportWidget(QWidget): self.toggle_state = False self.hw_cc = None self.is_learning = False - self.current_color = "transparent" + self.current_color = PALETTE_HEX.get("Gray", "#263238") self.color_name = "Gray" self.on_select_callback = None self.on_zone_change_callback = None self._label = label - self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Preferred) + self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Maximum) self.setFixedWidth(100) outer = QVBoxLayout(self) @@ -57,6 +57,7 @@ class TransportWidget(QWidget): self.cc_input_lbl = QLabel("") self.cc_input_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.cc_input_lbl.setStyleSheet("background-color: #000000; color: #888888; font-size: 10px; padding: 1px;") + self.cc_input_lbl.setFixedHeight(16) inner.addWidget(self.cc_input_lbl) # CC spin @@ -71,6 +72,7 @@ class TransportWidget(QWidget): self.cc_output_lbl = QLabel(f"Out CC{default_cc} [--]") self.cc_output_lbl.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.cc_output_lbl.setStyleSheet("background-color: #000000; color: #00e676; font-size: 10px; padding: 1px;") + self.cc_output_lbl.setFixedHeight(16) inner.addWidget(self.cc_output_lbl) # MIDI / OSC mode selector @@ -114,10 +116,9 @@ class TransportWidget(QWidget): # OSC address field (hidden by default) self.osc_addr_edit = QLineEdit("/play") self.osc_addr_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter) - self.osc_addr_edit.setFixedWidth(75) self.osc_addr_edit.setPlaceholderText("/address") self.osc_addr_edit.setVisible(False) - inner.addWidget(self.osc_addr_edit, alignment=Qt.AlignmentFlag.AlignHCenter) + inner.addWidget(self.osc_addr_edit) self.output_mode = "midi" # "midi" or "osc" @@ -140,6 +141,7 @@ class TransportWidget(QWidget): self.title_edit = QLineEdit(label) self.title_edit.setAlignment(Qt.AlignmentFlag.AlignHCenter) self.title_edit.setStyleSheet(TITLE_STYLE) + self.title_edit.textChanged.connect(self.btn.setText) inner.addWidget(self.title_edit) # Color swatch @@ -170,10 +172,10 @@ class TransportWidget(QWidget): inner.addWidget(self.zone_btn) # Separator + ID label - id_sep = QWidget() - id_sep.setFixedHeight(1) - id_sep.setStyleSheet("background-color: #3a3a3a;") - inner.addWidget(id_sep) + self.id_sep = QWidget() + self.id_sep.setFixedHeight(1) + self.id_sep.setStyleSheet("background-color: #3a3a3a;") + inner.addWidget(self.id_sep) self.id_label = QLabel("") self.id_label.setAlignment(Qt.AlignmentFlag.AlignHCenter) diff --git a/zonebutton.py b/zonebutton.py index a43f88f..a942fa9 100644 --- a/zonebutton.py +++ b/zonebutton.py @@ -38,7 +38,7 @@ class ZoneButton(QPushButton): self._update_style() def _update_style(self): - l_color = "#00c853" if self.left_active else "#2a2a2a" + l_color = "#ff8c00" if self.left_active else "#2a2a2a" r_color = "#00c853" if self.right_active else "#2a2a2a" self.setStyleSheet(f"""