diff --git a/src/midi.py b/src/midi.py index 7dc0675..811e380 100644 --- a/src/midi.py +++ b/src/midi.py @@ -59,6 +59,12 @@ class MidiHandler: self.current_bpm: float | None = None self.current_pattern: str = "" self.beat_index: int = 0 + + # Rate limiting for parameter updates + self.last_param_update: float = 0.0 + self.param_update_interval: float = 0.1 # 100ms minimum between updates + self.pending_param_update: bool = False + # Sequential pulse pattern state self.sequential_pulse_enabled: bool = False self.sequential_pulse_step: int = 0 @@ -79,6 +85,7 @@ class MidiHandler: # Create minimal payload - only send pattern payload = { "d": { # Defaults - only pattern + "t": "b", # Message type: beat "pt": "p", # pulse } } @@ -100,6 +107,7 @@ class MidiHandler: # Create minimal payload - only send what changes payload = { "d": { # Defaults - only pattern and n1/n2 + "t": "b", # Message type: beat "pt": "a", # alternating "n1": self.n1, "n2": self.n2, @@ -126,6 +134,7 @@ class MidiHandler: # Calculate packet size for full parameters full_payload = { "d": { + "t": "u", # Message type: update "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), "dl": self.delay, "cl": [self._current_color_rgb()], @@ -144,6 +153,7 @@ class MidiHandler: # Packet 1: Pattern and timing parameters payload1 = { "d": { + "t": "u", # Message type: update "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), "dl": self.delay, "br": self.brightness, @@ -155,6 +165,7 @@ class MidiHandler: # Packet 2: Color and pattern parameters payload2 = { "d": { + "t": "u", # Message type: update "cl": [self._current_color_rgb()], "n1": self.n1, "n2": self.n2, @@ -176,10 +187,26 @@ class MidiHandler: logging.debug(f"[Full Params] Sending single packet ({payload_size} bytes)") await self.ws_client.send_data(full_payload) + async def _request_param_update(self): + """Request a parameter update with rate limiting""" + import time + current_time = time.time() + + if current_time - self.last_param_update >= self.param_update_interval: + # Can send immediately + self.last_param_update = current_time + await self._send_full_parameters() + logging.debug("[Rate Limit] Parameter update sent immediately") + else: + # Rate limited - mark as pending + self.pending_param_update = True + logging.debug("[Rate Limit] Parameter update queued (rate limited)") + async def _send_normal_pattern(self): """Send normal pattern to all bars - minimal payload for beats""" payload = { "d": { # Defaults - only pattern + "t": "b", # Message type: beat "pt": PATTERN_NAMES.get(self.current_pattern, self.current_pattern), } } @@ -233,6 +260,16 @@ class MidiHandler: if self.beat_index % 8 == 0: await self._send_full_parameters() + # Check for pending parameter updates (rate limited) + if self.pending_param_update: + import time + current_time = time.time() + if current_time - self.last_param_update >= self.param_update_interval: + self.last_param_update = current_time + self.pending_param_update = False + await self._send_full_parameters() + logging.debug("[Rate Limit] Pending parameter update sent") + if self.current_pattern == "sequential_pulse": # Sequential pulse pattern: each bar pulses for 1 beat, then next bar, mirrored await self._handle_sequential_pulse() @@ -369,9 +406,11 @@ class MidiHandler: case 36: self.n3 = max(1, msg.value) # Update n3 step rate logging.info(f"n3 set to {self.n3} by MIDI controller (CC36)") + await self._request_param_update() case 37: self.delay = msg.value * 4 # Update instance delay logging.info(f"Delay set to {self.delay} ms by MIDI controller (CC37)") + await self._request_param_update() case 27: if msg.value == 127: self.beat_sending_enabled = True @@ -387,24 +426,30 @@ class MidiHandler: # 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 (CC33)") + await self._request_param_update() case 30: # Red 0-127 -> 0-255 self.color_r = round((msg.value / 127) * 255) logging.info(f"Red set to {self.color_r}") + await self._request_param_update() case 31: # Green 0-127 -> 0-255 self.color_g = round((msg.value / 127) * 255) logging.info(f"Green set to {self.color_g}") + await self._request_param_update() case 32: # Blue 0-127 -> 0-255 self.color_b = round((msg.value / 127) * 255) logging.info(f"Blue set to {self.color_b}") + await self._request_param_update() case 34: self.n1 = int(msg.value) logging.info(f"n1 set to {self.n1} by MIDI controller (CC34)") + await self._request_param_update() case 35: self.n2 = int(msg.value) logging.info(f"n2 set to {self.n2} by MIDI controller (CC35)") + await self._request_param_update() await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop