Update docs and screenshots
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
CI / backend (pull_request) Canceled after 0s
CI / shell (pull_request) Canceled after 0s
CI / frontend (pull_request) Canceled after 0s
CI / arm64-smoke (pull_request) Canceled after 0s
CI / backend (push) Canceled after 0s
CI / shell (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / arm64-smoke (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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
|
||||
Reference in New Issue
Block a user