Implement full parameter sending on pattern change and periodic updates
- Send all parameters when pattern changes (may require 2 packets if >200 bytes) - Send periodic parameter updates every 8 beats to keep bars synchronized - Beat packets remain minimal for performance - All packets stay under 230-byte limit
This commit is contained in:
175
src/midi.py
175
src/midi.py
@@ -7,6 +7,19 @@ 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
|
||||
from bar_config import LED_BAR_NAMES, DEFAULT_BAR_SETTINGS
|
||||
|
||||
# Pattern name mapping for shorter JSON payloads
|
||||
PATTERN_NAMES = {
|
||||
"flicker": "f",
|
||||
"fill_range": "fr",
|
||||
"n_chase": "nc",
|
||||
"alternating": "a",
|
||||
"pulse": "p",
|
||||
"rainbow": "r",
|
||||
"specto": "s",
|
||||
"radiate": "rd",
|
||||
}
|
||||
|
||||
|
||||
# Configure logging
|
||||
@@ -46,6 +59,9 @@ class MidiHandler:
|
||||
self.current_bpm: float | None = None
|
||||
self.current_pattern: str = ""
|
||||
self.beat_index: int = 0
|
||||
# Sequential pulse pattern state
|
||||
self.sequential_pulse_enabled: bool = False
|
||||
self.sequential_pulse_step: int = 0
|
||||
|
||||
def _current_color_rgb(self) -> tuple:
|
||||
r = max(0, min(255, int(self.color_r)))
|
||||
@@ -53,6 +69,128 @@ class MidiHandler:
|
||||
b = max(0, min(255, int(self.color_b)))
|
||||
return (r, g, b)
|
||||
|
||||
async def _handle_sequential_pulse(self):
|
||||
"""Handle sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored"""
|
||||
from bar_config import LEFT_BARS, RIGHT_BARS
|
||||
|
||||
# Calculate which bar should pulse based on beat (1 beat per bar)
|
||||
bar_index = self.beat_index % 4 # 0-3, cycles every 4 beats
|
||||
|
||||
# Create minimal payload - only send pattern
|
||||
payload = {
|
||||
"d": { # Defaults - only pattern
|
||||
"pt": "p", # pulse
|
||||
}
|
||||
}
|
||||
|
||||
# Set specific bars to pulse
|
||||
left_bar = LEFT_BARS[bar_index]
|
||||
right_bar = RIGHT_BARS[bar_index]
|
||||
|
||||
payload[left_bar] = {"pt": "p"} # pulse
|
||||
payload[right_bar] = {"pt": "p"} # pulse
|
||||
|
||||
logging.debug(f"[Sequential Pulse] Beat {self.beat_index}, pulsing bars {left_bar} and {right_bar}")
|
||||
await self.ws_client.send_data(payload)
|
||||
|
||||
async def _handle_alternating_phase(self):
|
||||
"""Handle alternating pattern with phase offset: every second bar swaps n1 and n2"""
|
||||
from bar_config import LED_BAR_NAMES
|
||||
|
||||
# Create minimal payload - only send what changes
|
||||
payload = {
|
||||
"d": { # Defaults - only pattern and n1/n2
|
||||
"pt": "a", # alternating
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
}
|
||||
}
|
||||
|
||||
# Set n1/n2 swap for every second bar (bars 101, 103, 105, 107)
|
||||
swap_bars = ["101", "103", "105", "107"]
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
if bar_name in swap_bars:
|
||||
# Swap n1 and n2 for out-of-phase bars
|
||||
payload[bar_name] = {"n1": self.n2, "n2": self.n1}
|
||||
else:
|
||||
# In-phase bars use defaults (no override needed)
|
||||
payload[bar_name] = {}
|
||||
|
||||
logging.debug(f"[Alternating Phase] Beat {self.beat_index}, n1/n2 swap for bars {swap_bars}")
|
||||
await self.ws_client.send_data(payload)
|
||||
|
||||
async def _send_full_parameters(self):
|
||||
"""Send all parameters to bars - may require multiple packets due to size limit"""
|
||||
from bar_config import LED_BAR_NAMES
|
||||
|
||||
# Calculate packet size for full parameters
|
||||
full_payload = {
|
||||
"d": {
|
||||
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
||||
"dl": self.delay,
|
||||
"cl": [self._current_color_rgb()],
|
||||
"br": self.brightness,
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"n3": self.n3,
|
||||
}
|
||||
}
|
||||
|
||||
# Estimate size: ~200 bytes for defaults + 8 bars * 2 bytes = ~216 bytes
|
||||
# This should fit in one packet, but let's be safe
|
||||
payload_size = len(str(full_payload))
|
||||
|
||||
if payload_size > 200: # Split into 2 packets if too large
|
||||
# Packet 1: Pattern and timing parameters
|
||||
payload1 = {
|
||||
"d": {
|
||||
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
||||
"dl": self.delay,
|
||||
"br": self.brightness,
|
||||
}
|
||||
}
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
payload1[bar_name] = {}
|
||||
|
||||
# Packet 2: Color and pattern parameters
|
||||
payload2 = {
|
||||
"d": {
|
||||
"cl": [self._current_color_rgb()],
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"n3": self.n3,
|
||||
}
|
||||
}
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
payload2[bar_name] = {}
|
||||
|
||||
logging.debug(f"[Full Params] Sending in 2 packets due to size ({payload_size} bytes)")
|
||||
await self.ws_client.send_data(payload1)
|
||||
await asyncio.sleep(0.01) # Small delay between packets
|
||||
await self.ws_client.send_data(payload2)
|
||||
else:
|
||||
# Single packet
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
full_payload[bar_name] = {}
|
||||
|
||||
logging.debug(f"[Full Params] Sending single packet ({payload_size} bytes)")
|
||||
await self.ws_client.send_data(full_payload)
|
||||
|
||||
async def _send_normal_pattern(self):
|
||||
"""Send normal pattern to all bars - minimal payload for beats"""
|
||||
payload = {
|
||||
"d": { # Defaults - only pattern
|
||||
"pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern),
|
||||
}
|
||||
}
|
||||
|
||||
# Add empty entries for each bar (they'll use defaults)
|
||||
for bar_name in LED_BAR_NAMES:
|
||||
payload[bar_name] = {}
|
||||
|
||||
logging.debug(f"[Beat] Triggering '{self.current_pattern}' for {len(LED_BAR_NAMES)} bars using defaults")
|
||||
await self.ws_client.send_data(payload)
|
||||
|
||||
async def _send_reset_to_sound(self):
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(self.sound_control_host, self.sound_control_port)
|
||||
@@ -90,22 +228,20 @@ class MidiHandler:
|
||||
logging.debug("[Beat] No pattern selected yet; ignoring beat")
|
||||
else:
|
||||
self.beat_index = (self.beat_index + 1) % 1000000
|
||||
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)
|
||||
|
||||
# Send periodic parameter updates every 8 beats
|
||||
if self.beat_index % 8 == 0:
|
||||
await self._send_full_parameters()
|
||||
|
||||
if self.current_pattern == "sequential_pulse":
|
||||
# Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored
|
||||
await self._handle_sequential_pulse()
|
||||
elif self.current_pattern == "alternating_phase":
|
||||
# Alternating pattern with phase offset: every second bar is out of phase
|
||||
await self._handle_alternating_phase()
|
||||
elif self.current_pattern:
|
||||
# Normal pattern mode - run on all bars
|
||||
await self._send_normal_pattern()
|
||||
except ValueError:
|
||||
logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning
|
||||
except Exception as e:
|
||||
@@ -207,10 +343,10 @@ class MidiHandler:
|
||||
("flicker", {}),
|
||||
("alternating", {"n1": 6, "n2": 6}),
|
||||
("n_chase", {"n1": 5, "n2": 5}),
|
||||
# fill_range intentionally omitted from buttons
|
||||
("rainbow", {}),
|
||||
# specto intentionally omitted from buttons
|
||||
("radiate", {"n1": 8}),
|
||||
("sequential_pulse", {}),
|
||||
("alternating_phase", {}),
|
||||
]
|
||||
idx = msg.note - 36
|
||||
if 0 <= idx < len(pattern_bindings):
|
||||
@@ -222,6 +358,9 @@ class MidiHandler:
|
||||
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})")
|
||||
|
||||
# Send full parameters when pattern changes
|
||||
await self._send_full_parameters()
|
||||
else:
|
||||
logging.debug(f"Note {msg.note} not bound to patterns")
|
||||
|
||||
|
Reference in New Issue
Block a user