Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Fix creating multiple MIDI interfaces #27

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion midi_app_controller/actions/_tests/test_actions_handler.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 3 additions & 2 deletions midi_app_controller/actions/actions_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down
53 changes: 26 additions & 27 deletions midi_app_controller/controller/connected_controller.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import time
from typing import List
from typing import List, Tuple

import rtmidi

Expand All @@ -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.
Expand Down Expand Up @@ -75,7 +53,6 @@
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
Expand All @@ -84,11 +61,32 @@
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

Check warning on line 79 in midi_app_controller/controller/connected_controller.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/controller/connected_controller.py#L79

Added line #L79 was not covered by tests

command = message[0] & 0xF0
channel = message[0] & 0x0F
data_bytes = message[1:]

Check warning on line 83 in midi_app_controller/controller/connected_controller.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/controller/connected_controller.py#L81-L83

Added lines #L81 - L83 were not covered by tests

logging.debug(

Check warning on line 85 in midi_app_controller/controller/connected_controller.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/controller/connected_controller.py#L85

Added line #L85 was not covered by tests
f"Received command: {command}, channel: {channel}, data: {data_bytes}"
)

self.handle_midi_message(command, channel, data_bytes)

Check warning on line 89 in midi_app_controller/controller/connected_controller.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/controller/connected_controller.py#L89

Added line #L89 was not covered by tests

def init_buttons(self) -> None:
"""Initializes the buttons on the controller, setting them
Expand Down Expand Up @@ -171,8 +169,9 @@
"""
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}")

Check warning on line 174 in midi_app_controller/controller/connected_controller.py

View check run for this annotation

Codecov / codecov/patch

midi_app_controller/controller/connected_controller.py#L174

Added line #L174 was not covered by tests

def flash_knob(self, id: int) -> None:
"""Flashes the LEDs corresponding to a knob on a MIDI controller.
Expand Down
11 changes: 2 additions & 9 deletions midi_app_controller/state/state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading