initial commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
venv/
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
"""
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user