initial commit

This commit is contained in:
Paul Lipscomb
2026-06-16 22:37:38 -04:00
commit 3e2147fa02
11 changed files with 2867 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
venv/
__pycache__/
*.pyc
.DS_Store
+31
View File
@@ -0,0 +1,31 @@
from PyQt6.QtWidgets import QWidget, QHBoxLayout, QPushButton
from PyQt6.QtCore import Qt
from palette import PALETTE
class ColorSwatchPopup(QWidget):
def __init__(self, callback, parent=None):
super().__init__(parent, Qt.WindowType.Popup)
self.callback = callback
layout = QHBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(4)
for name, hex_color in PALETTE:
btn = QPushButton()
btn.setFixedSize(22, 22)
btn.setStyleSheet(f"""
QPushButton {{
background-color: {hex_color};
border-radius: 3px;
border: 1px solid rgba(255,255,255,0.2);
}}
QPushButton:hover {{
border: 2px solid #00ff88;
}}
""")
btn.clicked.connect(lambda checked, n=name, c=hex_color: self.pick(n, c))
layout.addWidget(btn)
self.setStyleSheet("background-color: #2d2d2d; border-radius: 4px;")
def pick(self, name, hex_color):
self.callback(name, hex_color)
self.close()
+2473
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
import rtmidi
def get_available_input_ports():
"""Returns list of (display_name, original_index, available) for all ports."""
try:
all_ports = rtmidi.MidiIn().get_ports()
except:
return []
result = []
for i, name in enumerate(all_ports):
try:
test = rtmidi.MidiIn()
test.open_port(i)
test.close_port()
del test
result.append((name, i, True))
except:
print(f"Port in use, skipping: {name}")
result.append((f"{name} [in use by another program]", i, False))
return result
def open_midi_input_port(original_index, callback):
midi_in = rtmidi.MidiIn()
midi_in.open_port(original_index)
midi_in.set_callback(callback)
return midi_in
def close_midi_input_port(midi_in):
if midi_in and midi_in.is_port_open():
midi_in.close_port()
+11
View File
@@ -0,0 +1,11 @@
from PyQt6.QtCore import QObject, pyqtSignal
class MidiReceiver(QObject):
midi_in_signal = pyqtSignal(list)
midi_out_signal = pyqtSignal()
transport_out_signal = pyqtSignal()
hw_cc_signal = pyqtSignal(str, int)
midi_receiver = MidiReceiver()
+25
View File
@@ -0,0 +1,25 @@
import rtmidi
from midi_receiver import midi_receiver
MIDI_CH = 0
TRANSPORT_MIDI_CH = 0
midi_out = rtmidi.MidiOut()
transport_midi_out = rtmidi.MidiOut()
available_ports = midi_out.get_ports()
print("Available MIDI ports:", available_ports)
def send_cc(cc_number, value):
if midi_out.is_port_open():
midi_out.send_message([0xB0 | MIDI_CH, cc_number, value])
midi_receiver.midi_out_signal.emit()
#print([0xB0 | MIDI_CH, cc_number, value])
def send_transport_cc(cc_number, value):
if transport_midi_out.is_port_open():
transport_midi_out.send_message([0xB0 | TRANSPORT_MIDI_CH, cc_number, value])
midi_receiver.transport_out_signal.emit()
+45
View File
@@ -0,0 +1,45 @@
PALETTE = [
("Red", "#e53935"),
("Orange", "#fb8c00"),
("Yellow", "#fdd835"),
("Green", "#43a047"),
("Teal", "#00897b"),
("Blue", "#1e88e5"),
("Purple", "#8e24aa"),
("Pink", "#e91e63"),
("White", "#f5f5f5"),
("Gray", "#4a5568"),
]
PALETTE_NAMES = [p[0] for p in PALETTE]
PALETTE_HEX = {p[0]: p[1] for p in PALETTE}
PALETTE_VIVID = {
"Red": "#7f0000",
"Orange": "#7f3000",
"Yellow": "#7f6000",
"Green": "#1b5e20",
"Teal": "#004d40",
"Blue": "#0d2b6e",
"Purple": "#4a0072",
"Pink": "#7f004d",
"White": "#9e9e9e",
"Gray": "#263238",
}
def lighten_hex(hex_color, amount=0):
hex_color = hex_color.lstrip("#")
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
r = min(255, r + amount)
g = min(255, g + amount)
b = min(255, b + amount)
return f"#{r:02x}{g:02x}{b:02x}"
def darken_hex(hex_color, amount=60):
hex_color = hex_color.lstrip("#")
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
r = max(0, r - amount)
g = max(0, g - amount)
b = max(0, b - amount)
return f"#{r:02x}{g:02x}{b:02x}"
+17
View File
@@ -0,0 +1,17 @@
import json
import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PRESETS_DIR = os.path.join(BASE_DIR, "presets")
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:
return json.load(f)
return {"__last_used__": None, "presets": {}}
def save_presets_file(data):
with open(PRESETS_FILE, "w") as f:
json.dump(data, f, indent=2)
+105
View File
@@ -0,0 +1,105 @@
{
"__last_used__": "test1",
"presets": {
"test1": {
"midi_port": "Microsoft GS Wavetable Synth 0",
"midi_input": "MPKmini2 1",
"transport_port": "Microsoft GS Wavetable Synth 0",
"faders": [
{
"uid": "5ca2258a-24da-41b1-8d4a-464b52fc620a",
"label": "Fader 1",
"cc": 1,
"value": 102,
"color": "Green",
"pickup": false,
"last_sent": 102,
"hw_cc": null,
"zone": null,
"center_index": null,
"zone_index": null
}
],
"toggles": [
{
"uid": "2e87ec73-fcdc-4e7f-841b-9b701d03a576",
"label": "Toggle 1",
"cc": 20,
"state": false,
"color": "Blue",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null
}
],
"transports": [
{
"uid": "c5e25c55-f0b3-464f-aba9-7ff44963543b",
"label": "Play",
"cc": 117,
"state": false,
"color": "Green",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "2b2b4e59-643a-44dc-ad54-71f565ad3bc7",
"label": "Stop",
"cc": 116,
"state": false,
"color": "Yellow",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "b2b81948-59c9-4b1f-b185-d7ab244a195b",
"label": "Record",
"cc": 118,
"state": false,
"color": "Red",
"zone": null,
"center_index": null,
"zone_index": null,
"hw_cc": null,
"output_mode": "osc",
"osc_addr": "/play",
"trigger_mode": "toggle"
},
{
"uid": "e27dec15-9de0-4eb6-a776-d3d4bc216cb8",
"label": "Arm",
"cc": 119,
"state": false,
"color": "Red",
"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
}
}
}
}
+32
View File
@@ -0,0 +1,32 @@
FADER_STYLE = """
QSlider::groove:vertical {
background: #1e1e1e;
width: 8px;
border-radius: 4px;
}
QSlider::handle:vertical {
background: #bbbbbb;
border: 1px solid #222222;
height: 39px;
width: 24px;
margin: 0 -8px;
border-radius: 3px;
}
QSlider::handle:vertical:hover {
background: #dddddd;
border: 1px solid #111111;
}
"""
TITLE_STYLE = """
QLineEdit {
background-color: #000000;
border: none;
color: #ffffff;
font-weight: bold;
padding: 2px;
}
QLineEdit:focus {
border: 1px solid #555;
}
"""
+88
View File
@@ -0,0 +1,88 @@
from PyQt6.QtWidgets import QPushButton
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPainter, QColor, QFont
class ZoneButton(QPushButton):
"""Split L/R zone button. Left half = L, Right half = R."""
def __init__(self, parent=None):
super().__init__(parent)
self.left_active = False
self.right_active = False
self.setFixedHeight(20)
self.left_callback = None
self.right_callback = None
self._update_style()
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
if event.position().x() < self.width() / 2:
self.left_active = not self.left_active
if self.left_active:
self.right_active = False
if self.left_callback:
self.left_callback(self.left_active)
else:
self.right_active = not self.right_active
if self.right_active:
self.left_active = False
if self.right_callback:
self.right_callback(self.right_active)
self._update_style()
def set_state(self, left, right):
self.left_active = left
self.right_active = right
self._update_style()
def _update_style(self):
l_color = "#00c853" if self.left_active else "#2a2a2a"
r_color = "#00c853" if self.right_active else "#2a2a2a"
self.setStyleSheet(f"""
QPushButton {{
background: qlineargradient(
x1:0, y1:0, x2:1, y2:0,
stop:0 {l_color},
stop:0.499 {l_color},
stop:0.5 {r_color},
stop:1 {r_color}
);
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border: 1px solid #444;
color: #aaaaaa;
font-size: 9px;
font-weight: bold;
}}
""")
self.setText("")
self.update()
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
mid = self.width() // 2
painter.setPen(QColor("#666666"))
painter.drawLine(mid, 3, mid, self.height() - 3)
painter.setPen(QColor("#ffffff"))
font = QFont()
font.setPointSize(7)
font.setBold(True)
painter.setFont(font)
painter.drawText(0, 0, mid, self.height(), 0x0084, "L")
painter.drawText(mid, 0, mid, self.height(), 0x0084, "R")
painter.end()