From 8d0c9edf5dc38f9a4dde8449fa628e272692bbb7 Mon Sep 17 00:00:00 2001 From: jimmy Date: Wed, 17 Sep 2025 20:22:11 +1200 Subject: [PATCH] ws: adopt nested {'0': {...}} payloads midi: bind patterns to notes 36+; beat triggers selected pattern; include n index; CC map: 30=R 31=G 32=B 33=brightness 34=n1 35=n2 36=delay; send n1/n2 raw 0-127 gui: show n1 and n2 in status --- src/main.py | 8 ++- src/midi.py | 132 ++++++++++++++++++++++++------------------------ src/settings.py | 27 ---------- 3 files changed, 73 insertions(+), 94 deletions(-) delete mode 100644 src/settings.py diff --git a/src/main.py b/src/main.py index f8d1d47..5cad64a 100644 --- a/src/main.py +++ b/src/main.py @@ -60,9 +60,11 @@ class App: 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: -") - for w in (self.lbl_delay, self.lbl_brightness, self.lbl_r, self.lbl_g, self.lbl_b, self.lbl_pattern, self.lbl_bpm): + 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") # schedule periodic UI updates @@ -98,6 +100,8 @@ class App: 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 "-" @@ -107,6 +111,8 @@ class App: 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}") # reschedule diff --git a/src/midi.py b/src/midi.py index 521a9b8..072bba9 100644 --- a/src/midi.py +++ b/src/midi.py @@ -5,6 +5,9 @@ import socket import json import logging # Added logging import import time # Added for initial state read +import tkinter as tk +from tkinter import ttk, messagebox # Import messagebox for confirmations + # Configure logging DEBUG_MODE = True # Set to False for INFO level logging @@ -34,9 +37,14 @@ class MidiHandler: self.color_r = 0 self.color_g = 255 self.color_b = 0 + # Generic parameters controlled via CC + # Raw CC-driven parameters (0-127) + self.n1 = 10 + self.n2 = 10 # 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: r = max(0, min(255, int(self.color_r))) @@ -76,19 +84,25 @@ class MidiHandler: # Attempt to parse as float (BPM) from sound.py bpm_value = float(message) self.current_bpm = bpm_value - # Construct JSON message using the current MIDI-controlled delay and brightness - json_message = { - "names": ["0"], - "settings": { - "pattern": "pulse", - "delay": self.delay, # Use MIDI-controlled delay - "colors": [self._current_color_hex()], - "brightness": self.brightness, - "num_leds": 200, - }, - } - logging.debug(f"[MidiHandler - TCP Server] Forwarding BPM-derived JSON message to WebSocket with delay {self.delay}, brightness {self.brightness}: {json_message}") # Changed to debug - await self.ws_client.send_data(json_message) + # On each beat, trigger currently selected pattern + 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) except ValueError: logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning except Exception as e: @@ -124,9 +138,9 @@ class MidiHandler: if msg.control == 36: self.delay = msg.value * 4 logging.info(f"[Init] Delay set to {self.delay} ms from CC36") - elif msg.control == 37: + elif msg.control == 33: self.brightness = round((msg.value / 127) * 100) - logging.info(f"[Init] Brightness set to {self.brightness} from CC37") + logging.info(f"[Init] Brightness set to {self.brightness} from CC33") elif msg.control == 30: self.color_r = round((msg.value / 127) * 255) logging.info(f"[Init] Red set to {self.color_r} from CC30") @@ -136,6 +150,12 @@ class MidiHandler: elif msg.control == 32: self.color_b = round((msg.value / 127) * 255) logging.info(f"[Init] Blue set to {self.color_b} from CC32") + elif msg.control == 34: + self.n1 = int(msg.value) + logging.info(f"[Init] n1 set to {self.n1} from CC34") + elif msg.control == 35: + self.n2 = int(msg.value) + logging.info(f"[Init] n2 set to {self.n2} from CC35") elif msg.control == 27: self.beat_sending_enabled = (msg.value == 127) logging.info(f"[Init] Beat sending {'ENABLED' if self.beat_sending_enabled else 'DISABLED'} from CC27") @@ -175,55 +195,29 @@ 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 - pattern_name = "Unknown" - - match msg.note: - case 48: # Original Note 48 for 'pulse' - pattern_name = "pulse" - self.current_pattern = pattern_name - await self.ws_client.send_data({ - "names": ["0"], - "settings": { - "pattern": pattern_name, - "delay": self.delay, # Use MIDI-controlled delay - "colors": [self._current_color_hex()], - "brightness": self.brightness, - "num_leds": 120, - } - }) - case 49: # Original Note 49 for 'theater_chase' - pattern_name = "theater_chase" - self.current_pattern = pattern_name - await self.ws_client.send_data({ - "names": ["0"], - "settings": { - "pattern": pattern_name, - "delay": self.delay, # Use MIDI-controlled delay - "colors": [self._current_color_hex()], - "brightness": self.brightness, - "num_leds": 120, - "on_width": 10, - "off_width": 10, - "n1": 0, - "n2": 100 - } - }) - case 50: # Original Note 50 for 'alternating' - pattern_name = "alternating" - self.current_pattern = pattern_name - logging.debug("Triggering Alternating Pattern") # Changed to debug - await self.ws_client.send_data({ - "names": ["0"], - "settings": { - "pattern": pattern_name, - "delay": self.delay, # Use MIDI-controlled delay - "colors": [self._current_color_hex(), "#0000ff"], - "brightness": self.brightness, - "num_leds": 120, - "n1": 10, - "n2": 10 - } - }) + # Bind 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}), + ("rainbow", {}), + ("specto", {"n1": 20}), + ("radiate", {"n1": 8}), + ] + idx = msg.note - 36 + if 0 <= idx < len(pattern_bindings): + pattern_name, extra = pattern_bindings[idx] + self.current_pattern = pattern_name + # Apply any immediate param tweaks from binding to local state + if "n1" in extra: + self.n1 = extra["n1"] + if "n2" in extra: + 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)") case 'control_change': match msg.control: @@ -241,10 +235,10 @@ class MidiHandler: if msg.value == 127: logging.info("[MidiHandler - Listener] RESET_TEMPO requested by control 29.") await self._send_reset_to_sound() - case 37: + case 33: # Map 0-127 to 0-100 brightness scale self.brightness = round((msg.value / 127) * 100) - logging.info(f"Brightness set to {self.brightness} by MIDI controller") + logging.info(f"Brightness set to {self.brightness} by MIDI controller (CC33)") case 30: # Red 0-127 -> 0-255 self.color_r = round((msg.value / 127) * 255) @@ -257,6 +251,12 @@ class MidiHandler: # Blue 0-127 -> 0-255 self.color_b = round((msg.value / 127) * 255) logging.info(f"Blue set to {self.color_b}") + case 34: + self.n1 = int(msg.value) + logging.info(f"n1 set to {self.n1} by MIDI controller (CC34)") + case 35: + self.n2 = int(msg.value) + logging.info(f"n2 set to {self.n2} by MIDI controller (CC35)") await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop diff --git a/src/settings.py b/src/settings.py deleted file mode 100644 index 571cd3b..0000000 --- a/src/settings.py +++ /dev/null @@ -1,27 +0,0 @@ -import json - -class Settings(dict): - SETTINGS_FILE = "settings.json" - - def __init__(self): - super().__init__() - self.load() # Load settings from file during initialization - - def save(self): - try: - j = json.dumps(self, indent=4) - with open(self.SETTINGS_FILE, 'w') as file: - file.write(j) - print("Settings saved successfully.") - except Exception as e: - print(f"Error saving settings: {e}") - - def load(self): - try: - with open(self.SETTINGS_FILE, 'r') as file: - loaded_settings = json.load(file) - self.update(loaded_settings) - print("Settings loaded successfully.") - except Exception as e: - print(f"Error loading settings {e}") - self.save()