diff --git a/midi_app_controller/actions/_tests/test_actions_handler.py b/midi_app_controller/actions/_tests/test_actions_handler.py index 1b33f9a..ac3831d 100644 --- a/midi_app_controller/actions/_tests/test_actions_handler.py +++ b/midi_app_controller/actions/_tests/test_actions_handler.py @@ -1,7 +1,7 @@ from unittest.mock import Mock, call, patch # ruff: noqa: E402 -patch("superqt.utils.ensure_main_thread", lambda x: x).start() +patch("superqt.utils.ensure_main_thread", lambda await_return: lambda f: f).start() import pytest from app_model.types import Action diff --git a/midi_app_controller/actions/actions_handler.py b/midi_app_controller/actions/actions_handler.py index e480137..021256d 100644 --- a/midi_app_controller/actions/actions_handler.py +++ b/midi_app_controller/actions/actions_handler.py @@ -35,14 +35,15 @@ def get_knob_value(self, knob_id: int) -> Optional[int]: """Returns knob's value from the action associated with the knob.""" raise NotImplementedError # TODO - @ensure_main_thread + # Without `await_return` closing MIDI ports freezes after handling at least two actions. + @ensure_main_thread(await_return=True) def handle_button_action(self, button_id: int) -> None: """Executes an action associated with the button if it exists.""" action = self.bound_controller.get_button_press_action(button_id) if action is not None: self.app.commands.execute_command(action.id) - @ensure_main_thread + @ensure_main_thread(await_return=True) def handle_knob_action( self, *, diff --git a/midi_app_controller/controller/connected_controller.py b/midi_app_controller/controller/connected_controller.py index bd4259e..9e0f085 100644 --- a/midi_app_controller/controller/connected_controller.py +++ b/midi_app_controller/controller/connected_controller.py @@ -1,6 +1,6 @@ import logging import time -from typing import List +from typing import List, Tuple import rtmidi @@ -9,28 +9,6 @@ from .controller_constants import ControllerConstants -def midi_callback(message: List[int], cls: "ConnectedController") -> None: - """Callback function for MIDI input, specified by rtmidi package. - - Parameters - ---------- - message : List[int] - Standard MIDI message. - cls : ConnectedController - ConnectedController class instance. - """ - - # Process MIDI message here - status_byte = message[0][0] - command = status_byte & 0xF0 - channel = status_byte & 0x0F - data_bytes = message[0][1:] - - logging.debug(f"command: {command}, channel: {channel}, data: {data_bytes}") - - cls.handle_midi_message(command=command, channel=channel, data=data_bytes) - - class ConnectedController: """A controller connected to the physical device capable of sending and receiving signals. @@ -75,7 +53,6 @@ def __init__( midi_out: rtmidi.MidiOut Midi output client with the controller's port opened. """ - self.controller = controller self.actions_handler = actions_handler self.midi_out = midi_out @@ -84,11 +61,32 @@ def __init__( self.knob_ids = [element.id for element in controller.knobs] self.knob_engagement = {} + # Set default values for buttons and knobs. self.init_buttons() self.init_knobs() - # Set callback for getting data from controller - self.midi_in.set_callback(midi_callback, data=self) + # Set callback for getting data from controller. + self.midi_in.set_callback(self.midi_callback) + + def midi_callback(self, event: Tuple[List[int], float], data=None) -> None: + """Callback function for MIDI input, specified by rtmidi package. + + Parameters + ---------- + event : Tuple[List[int], float] + Pair of (MIDI message, delta time). + """ + message, _ = event + + command = message[0] & 0xF0 + channel = message[0] & 0x0F + data_bytes = message[1:] + + logging.debug( + f"Received command: {command}, channel: {channel}, data: {data_bytes}" + ) + + self.handle_midi_message(command, channel, data_bytes) def init_buttons(self) -> None: """Initializes the buttons on the controller, setting them @@ -171,8 +169,9 @@ def send_midi_message(self, data: List[int]) -> None: """ try: self.midi_out.send_message(data) + logging.debug(f"Sent: {data}") except ValueError as err: - print(f"Value Error: {err}") + logging.error(f"Value Error: {err}") def flash_knob(self, id: int) -> None: """Flashes the LEDs corresponding to a knob on a MIDI controller. diff --git a/midi_app_controller/state/state_manager.py b/midi_app_controller/state/state_manager.py index c3c850e..a54dbff 100644 --- a/midi_app_controller/state/state_manager.py +++ b/midi_app_controller/state/state_manager.py @@ -145,15 +145,8 @@ def stop_handling(self) -> None: """Stops handling any MIDI signals.""" self._midi_in.cancel_callback() self._connected_controller = None - - # TODO This does NOT work - it freezes. Using other methods like delete() - # does NOT work too. Removing "@ensure_main_thread" in the ActionHandler - # seems to fix this issue (but we need it for other reasons). - # self._midi_in.close_port() - # self._midi_out.close_port() - # Very dirty workaround (it does not close previously opened interfaces!): - self._midi_in = rtmidi.MidiIn() - self._midi_out = rtmidi.MidiOut() + self._midi_in.close_port() + self._midi_out.close_port() def start_handling(self) -> None: """Starts handling MIDI input using current values of binds, controller, etc.