from __future__ import annotations from dataclasses import dataclass, field from datetime import UTC, datetime from time import monotonic from typing import Any def list_input_devices() -> list[str]: import mido return list(mido.get_input_names()) def match_input_device(device_name: str, devices: list[str] | None = None) -> str | None: available = devices or list_input_devices() requested = device_name.casefold() for name in available: if requested in name.casefold(): return name return None def normalize_message(message: Any, bridge_id: str, device_name: str) -> dict[str, object] | None: message_type = getattr(message, "type", None) if message_type not in {"note_on", "note_off", "control_change", "program_change"}: return None number = getattr(message, "note", None) if message_type in {"control_change", "program_change"}: number = getattr(message, "control", None) if message_type == "control_change" else getattr(message, "program", None) if number is None: return None value = getattr(message, "velocity", None) if message_type == "control_change": value = getattr(message, "value", 0) if message_type == "program_change": value = getattr(message, "value", 127) if value is None: value = 0 return { "type": "midi_event", "protocol_version": 1, "bridge_id": bridge_id, "device": device_name, "timestamp": datetime.now(UTC).isoformat(), "message": { "type": message_type, "channel": int(getattr(message, "channel", 0)), "number": int(number), "value": int(value), }, } @dataclass(slots=True) class MidiEventFilter: channel_filter: int | None control_change_interval_ms: int suppress_duplicate_values: bool _last_sent_at: dict[tuple[str, int, int], float] = field(default_factory=dict, init=False) _last_values: dict[tuple[str, int, int], int] = field(default_factory=dict, init=False) def should_forward(self, payload: dict[str, object]) -> bool: message = payload["message"] if not isinstance(message, dict): return False message_type = str(message["type"]) channel = int(message["channel"]) number = int(message["number"]) value = int(message["value"]) if self.channel_filter is not None and channel != self.channel_filter: return False key = (message_type, channel, number) if self.suppress_duplicate_values and self._last_values.get(key) == value: return False if message_type == "control_change": now_ms = monotonic() * 1000 last_sent = self._last_sent_at.get(key, 0.0) if now_ms - last_sent < self.control_change_interval_ms: return False self._last_sent_at[key] = now_ms self._last_values[key] = value return True