From 36dfda74b2005cfb0992da2c49cfc8577830fd26 Mon Sep 17 00:00:00 2001 From: jimmy Date: Thu, 18 Sep 2025 20:35:31 +1200 Subject: [PATCH] Update GUI layout and MIDI CC mappings: CC36=n3, CC37=delay, remove B1/B2 references --- src/main.py | 157 +++++++++++++++++++++++++++++++++++++++++++--------- src/midi.py | 55 ++++++++++-------- 2 files changed, 164 insertions(+), 48 deletions(-) diff --git a/src/main.py b/src/main.py index 5cad64a..cceebf8 100644 --- a/src/main.py +++ b/src/main.py @@ -50,22 +50,90 @@ class App: "TNotebook.Tab", background=bg_color, foreground=fg_color, font=("Arial", 30), padding=[10, 5] ) - # --- Status Frame --- - status_frame = ttk.Frame(self.root) - status_frame.pack(padx=16, pady=16, fill="x") + # (Status box removed per request) - self.lbl_delay = ttk.Label(status_frame, text="Delay: -") - self.lbl_brightness = ttk.Label(status_frame, text="Brightness: -") - self.lbl_r = ttk.Label(status_frame, text="R: -") - self.lbl_g = ttk.Label(status_frame, text="G: -") - self.lbl_b = ttk.Label(status_frame, text="B: -") - self.lbl_pattern = ttk.Label(status_frame, text="Pattern: -") - self.lbl_n1 = ttk.Label(status_frame, text="n1: -") - self.lbl_n2 = ttk.Label(status_frame, text="n2: -") - self.lbl_bpm = ttk.Label(status_frame, text="BPM: -") + # Controls overview (dials grid left, buttons grids right) + controls_frame = ttk.Frame(self.root) + controls_frame.pack(padx=16, pady=8, fill="both") - for w in (self.lbl_delay, self.lbl_brightness, self.lbl_r, self.lbl_g, self.lbl_b, self.lbl_pattern, self.lbl_n1, self.lbl_n2, self.lbl_bpm): - w.pack(anchor="w") + # Dials box: 4 rows by 2 columns (top-left origin): + # Row0: n3 (left), Delay (right) + # Row1: n1 (left), n2 (right) + # Row2: B (left), Brightness (right) + # Row3: R (left), G (right) + dials_frame = ttk.LabelFrame(controls_frame, text="Dials (CC)") + dials_frame.pack(side="left", padx=12) + for c in range(2): + dials_frame.grid_columnconfigure(c, minsize=180) + for rr in range(4): + dials_frame.grid_rowconfigure(rr, minsize=100) + + self.dials_boxes: list[tk.Label] = [] + # Create with placeholders so they are visible before first update + placeholders = { + (0, 0): "n3\n-", + (0, 1): "Delay\n-", + (1, 0): "n1\n-", + (1, 1): "n2\n-", + (2, 0): "B\n-", + (2, 1): "Bright\n-", + (3, 0): "R\n-", + (3, 1): "G\n-", + } + for r in range(4): + for c in range(2): + lbl = tk.Label( + dials_frame, + text=placeholders.get((r, c), "-"), + bg=bg_color, + fg=fg_color, + font=("Arial", 16), + padx=6, + pady=6, + borderwidth=2, + relief="ridge", + width=20, + height=5, + anchor="center", + justify="center", + ) + lbl.grid(row=r, column=c, padx=6, pady=6, sticky="nsew") + self.dials_boxes.append(lbl) + + # Buttons bank (single) + buttons_frame = ttk.Frame(controls_frame) + buttons_frame.pack(side="left", padx=12) + + buttons1_frame = ttk.LabelFrame(buttons_frame, text="Buttons (notes 36-51)") + buttons1_frame.pack(side="top", pady=8) + for c in range(4): + buttons1_frame.grid_columnconfigure(c, minsize=110) + for rr in range(1, 5): + buttons1_frame.grid_rowconfigure(rr, minsize=110) + self.button1_cells: list[tk.Label] = [] + for r in range(4): + for c in range(4): + lbl = tk.Label( + buttons1_frame, + text="", + bg=bg_color, + fg=fg_color, + font=("Arial", 16), + padx=4, + pady=4, + borderwidth=2, + relief="ridge", + width=12, + height=6, + anchor="center", + justify="center", + ) + lbl.grid(row=1 + (3 - r), column=c, padx=6, pady=6, sticky="nsew") + self.button1_cells.append(lbl) + + # (No second buttons bank) + + # (No status labels to pack) # schedule periodic UI updates self.root.after(200, self.update_status_labels) @@ -99,21 +167,60 @@ class App: r = getattr(self.midi_handler, 'color_r', 0) g = getattr(self.midi_handler, 'color_g', 0) b = getattr(self.midi_handler, 'color_b', 0) + # Single bank values + brightness = getattr(self.midi_handler, 'brightness', '-') + r = getattr(self.midi_handler, 'color_r', 0) + g = getattr(self.midi_handler, 'color_g', 0) + b = getattr(self.midi_handler, 'color_b', 0) pattern = getattr(self.midi_handler, 'current_pattern', '') or '-' n1 = getattr(self.midi_handler, 'n1', '-') n2 = getattr(self.midi_handler, 'n2', '-') - bpm = getattr(self.midi_handler, 'current_bpm', None) - bpm_text = f"{bpm:.2f}" if isinstance(bpm, (float, int)) else "-" + n3 = getattr(self.midi_handler, 'n3', '-') - self.lbl_delay.config(text=f"Delay: {delay}") - self.lbl_brightness.config(text=f"Brightness: {brightness}") - self.lbl_r.config(text=f"R: {r}") - self.lbl_g.config(text=f"G: {g}") - self.lbl_b.config(text=f"B: {b}") - self.lbl_pattern.config(text=f"Pattern: {pattern}") - self.lbl_n1.config(text=f"n1: {n1}") - self.lbl_n2.config(text=f"n2: {n2}") - self.lbl_bpm.config(text=f"BPM: {bpm_text}") + # Update dials 2x4 grid (left→right, top→bottom): + # Row0: n3, Delay + # Row1: n1, n2 + # Row2: B, Brightness + # Row3: R, G + dial_values = [ + ("n3", n3), ("Delay", getattr(self.midi_handler, 'delay', '-')), + ("n1", n1), ("n2", n2), + ("B", b), ("Brightness", brightness), + ("R", r), ("G", g), + ] + # Update dial displays + for idx, (label, value) in enumerate(dial_values): + if idx < len(self.dials_boxes): + self.dials_boxes[idx].config(text=f"{label}\n{value}") + + # Update buttons bank mappings and selection (single bank) + # Pattern icons for nicer appearance + icon_for = { + "pulse": "💥", + "flicker": "✨", + "alternating": "↔️", + "n_chase": "🏃", + "rainbow": "🌈", + "radiate": "🌟", + "-": "", + } + bank1_patterns = [ + "pulse", "flicker", "alternating", "n_chase", + "rainbow", "radiate", "-", "-", + "-", "-", "-", "-", + "-", "-", "-", "-", + ] + # notes numbers per cell (bottom-left origin) + for idx, lbl in enumerate(self.button1_cells): + name = bank1_patterns[idx] + sel = (pattern == name and name != "-") + icon = icon_for.get(name, "") + text = f"{icon} {name}" if name != "-" else "" + if sel: + lbl.config(text=text, bg=highlight_pattern_color) + else: + lbl.config(text=text, bg=bg_color) + # (no second bank to update) # reschedule self.root.after(200, self.update_status_labels) diff --git a/src/midi.py b/src/midi.py index 072bba9..182ed89 100644 --- a/src/midi.py +++ b/src/midi.py @@ -41,16 +41,17 @@ class MidiHandler: # Raw CC-driven parameters (0-127) self.n1 = 10 self.n2 = 10 + self.n3 = 1 # Current state for GUI display self.current_bpm: float | None = None self.current_pattern: str = "" self.beat_index: int = 0 - def _current_color_hex(self) -> str: + def _current_color_rgb(self) -> tuple: r = max(0, min(255, int(self.color_r))) g = max(0, min(255, int(self.color_g))) b = max(0, min(255, int(self.color_b))) - return f"#{r:02x}{g:02x}{b:02x}" + return (r, g, b) async def _send_reset_to_sound(self): try: @@ -84,25 +85,27 @@ class MidiHandler: # Attempt to parse as float (BPM) from sound.py bpm_value = float(message) self.current_bpm = bpm_value - # On each beat, trigger currently selected pattern + # On each beat, trigger currently selected pattern(s) if not self.current_pattern: logging.debug("[Beat] No pattern selected yet; ignoring beat") else: - payload = { - "0": { - "pattern": self.current_pattern, - "delay": self.delay, - "colors": [self._current_color_hex()], - "brightness": self.brightness, - "num_leds": 200, - "n1": self.n1, - "n2": self.n2, - "n": self.beat_index, - } - } self.beat_index = (self.beat_index + 1) % 1000000 - logging.debug(f"[Beat] Triggering pattern '{self.current_pattern}' with payload: {payload}") - await self.ws_client.send_data(payload) + if self.current_pattern: + payload1 = { + "0": { + "pattern": self.current_pattern, + "delay": self.delay, + "colors": [self._current_color_rgb()], + "brightness": self.brightness, + "num_leds": 200, + "n1": self.n1, + "n2": self.n2, + "n3": self.n3, + "n": self.beat_index, + } + } + logging.debug(f"[Beat] Triggering '{self.current_pattern}' with payload: {payload1}") + await self.ws_client.send_data(payload1) except ValueError: logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning except Exception as e: @@ -136,8 +139,11 @@ class MidiHandler: msg = port.receive(block=False) if msg and msg.type == 'control_change': if msg.control == 36: + self.n3 = max(1, msg.value) + logging.info(f"[Init] n3 set to {self.n3} from CC36") + elif msg.control == 37: self.delay = msg.value * 4 - logging.info(f"[Init] Delay set to {self.delay} ms from CC36") + logging.info(f"[Init] Delay set to {self.delay} ms from CC37") elif msg.control == 33: self.brightness = round((msg.value / 127) * 100) logging.info(f"[Init] Brightness set to {self.brightness} from CC33") @@ -195,15 +201,15 @@ class MidiHandler: match msg.type: case 'note_on': logging.debug(f" Note ON: Note={msg.note}, Velocity={msg.velocity}, Channel={msg.channel}") # Changed to debug - # Bind patterns starting at MIDI note 36 + # Bank1 patterns starting at MIDI note 36 pattern_bindings: list[tuple[str, dict]] = [ ("pulse", {"n1": 120, "n2": 120}), ("flicker", {}), ("alternating", {"n1": 6, "n2": 6}), ("n_chase", {"n1": 5, "n2": 5}), - ("fill_range", {"n1": 10, "n2": 20}), + # fill_range intentionally omitted from buttons ("rainbow", {}), - ("specto", {"n1": 20}), + # specto intentionally omitted from buttons ("radiate", {"n1": 8}), ] idx = msg.note - 36 @@ -217,13 +223,16 @@ class MidiHandler: self.n2 = extra["n2"] logging.info(f"[Select] Pattern selected via note {msg.note}: {self.current_pattern} (n1={self.n1}, n2={self.n2})") else: - logging.debug(f"Note {msg.note} not bound to a pattern (base 36, {len(pattern_bindings)} entries)") + logging.debug(f"Note {msg.note} not bound to patterns") case 'control_change': match msg.control: case 36: + self.n3 = max(1, msg.value) # Update n3 step rate + logging.info(f"n3 set to {self.n3} by MIDI controller (CC36)") + case 37: self.delay = msg.value * 4 # Update instance delay - logging.info(f"Delay set to {self.delay} ms by MIDI controller") # Changed to info + logging.info(f"Delay set to {self.delay} ms by MIDI controller (CC37)") case 27: if msg.value == 127: self.beat_sending_enabled = True